diff --git a/.github/workflows/build_avalonia.yml b/.github/workflows/build_avalonia.yml new file mode 100644 index 0000000000..a12254c8f6 --- /dev/null +++ b/.github/workflows/build_avalonia.yml @@ -0,0 +1,25 @@ +name: build_avalonia + +on: + workflow_dispatch: + pull_request: + branches: + - master + paths: + - .github/workflows/build_forms.yml + - Directory.Build.props + - Directory.Build.targets + - Directory.Packages.props + - xunit.runner.json + - 'src/Prism.Core/**' + - 'src/Prism.Events/**' + - 'tests/Prism.Core.Tests/**' + - 'src/Avalonia/**' + - 'tests/Avalonia/**' + +jobs: + build-prism-avalonia: + uses: avantipoint/workflow-templates/.github/workflows/dotnet-build.yml@v1 + with: + name: Build Prism.Avalonia + solution-path: PrismLibrary_Avalonia.slnf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e69f3d750..da289c0a4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,20 @@ jobs: codeSignClientSecret: ${{ secrets.CodeSignClientSecret }} codeSignCertificate: ${{ secrets.CodeSignCertificate }} + build-prism-avalonia: + uses: avantipoint/workflow-templates/.github/workflows/dotnet-build.yml@v1 + with: + name: Build Prism.Avalonia + solution-path: PrismLibrary_Avalonia.slnf + code-sign: true + artifact-name: Avalonia + secrets: + codeSignKeyVault: ${{ secrets.CodeSignKeyVault }} + codeSignClientId: ${{ secrets.CodeSignClientId }} + codeSignTenantId: ${{ secrets.CodeSignTenantId }} + codeSignClientSecret: ${{ secrets.CodeSignClientSecret }} + codeSignCertificate: ${{ secrets.CodeSignCertificate }} + build-prism-uno: uses: avantipoint/workflow-templates/.github/workflows/msbuild-build.yml@v1 with: @@ -89,7 +103,7 @@ jobs: codeSignCertificate: ${{ secrets.CodeSignCertificate }} generate-consolidated-artifacts: - needs: [build-prism-core, build-prism-wpf, build-prism-uno, build-prism-maui] + needs: [build-prism-core, build-prism-wpf, build-prism-avalonia, build-prism-uno, build-prism-maui] runs-on: windows-latest steps: - name: Checkout @@ -107,6 +121,12 @@ jobs: name: Wpf path: artifacts\Wpf + - name: Download Avalonia + uses: actions/download-artifact@v3 + with: + name: Avalonia + path: artifacts\Avalonia + - name: Download Uno uses: actions/download-artifact@v3 with: diff --git a/Directory.Build.props b/Directory.Build.props index 1b57ba9bdc..551baef951 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -37,6 +37,7 @@ $(MSBuildProjectName.Contains('Uno')) $(MSBuildProjectName.Contains('Forms')) $(MSBuildProjectName.Contains('Maui')) + $(MSBuildProjectName.Contains('Avalonia')) True $(MSBuildThisFileDirectory)prism.snk False @@ -49,6 +50,7 @@ prism;mvvm;wpf;dependency injection;di prism;mvvm;winui;uno-platform;xamarin;webassembly;android;ios;macos;dependency injection;di prism;mvvm;uwp;android;ios;xamarin;xamarin.forms;dependency injection;di + prism;mvvm;axaml;xaml;desktop;navigation;prismavalonia;dialog;linux;macos;avalonia;dependency injection;di false false $(IsWpfProject) @@ -83,6 +85,22 @@ + + + $(DefineConstants);AVALONIA + + + + + + + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index aa251ad062..835a7f4104 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,7 +57,20 @@ - + + + + + + + + + + + + + + diff --git a/PrismLibrary.sln b/PrismLibrary.sln index 2d188338d3..5b2cc2f306 100644 --- a/PrismLibrary.sln +++ b/PrismLibrary.sln @@ -70,6 +70,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Events", "src\Prism.E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Uno.WinUI.Markup", "src\Uno\Prism.Uno.Markup\Prism.Uno.WinUI.Markup.csproj", "{0EA416B6-0AB6-464B-9F4D-206FFCFB262D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Avalonia", "Avalonia", "{8AE1015F-4D62-47EF-A576-2F7411EC20D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Avalonia", "src\Avalonia\Prism.Avalonia\Prism.Avalonia.csproj", "{C505C63F-99E6-464F-8C83-1AE4239C19B1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.DryIoc.Avalonia", "src\Avalonia\Prism.DryIoc.Avalonia\Prism.DryIoc.Avalonia.csproj", "{A6B7B19C-3288-4CD2-A737-527BEB1ED807}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Avalonia.Tests", "tests\Avalonia\Prism.Avalonia.Tests\Prism.Avalonia.Tests.csproj", "{AB501F63-8E8C-4333-8A15-81BA02F3C703}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Prism.Container.Avalonia.Shared", "tests\Avalonia\Prism.Container.Avalonia.Shared\Prism.Container.Avalonia.Shared.shproj", "{6FDA7D49-DF44-4E8F-A182-0EB73DD3C452}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.DryIoc.Avalonia.Tests", "tests\Avalonia\Prism.DryIoc.Avalonia.Tests\Prism.DryIoc.Avalonia.Tests.csproj", "{03B9C775-9582-409F-B67F-6B8A0CE0C7AF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.IocContainer.Avalonia.Tests.Support", "tests\Avalonia\Prism.IocContainer.Avalonia.Tests.Support\Prism.IocContainer.Avalonia.Tests.Support.csproj", "{887E0794-798D-4127-847E-6F68D5C9B334}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Avalonia", "Avalonia", "{904D5094-55F9-4581-90AC-D6C1131F8152}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -296,6 +312,66 @@ Global {0EA416B6-0AB6-464B-9F4D-206FFCFB262D}.Release|x64.Build.0 = Release|Any CPU {0EA416B6-0AB6-464B-9F4D-206FFCFB262D}.Release|x86.ActiveCfg = Release|Any CPU {0EA416B6-0AB6-464B-9F4D-206FFCFB262D}.Release|x86.Build.0 = Release|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Debug|x64.Build.0 = Debug|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Debug|x86.Build.0 = Debug|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Release|Any CPU.Build.0 = Release|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Release|x64.ActiveCfg = Release|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Release|x64.Build.0 = Release|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Release|x86.ActiveCfg = Release|Any CPU + {C505C63F-99E6-464F-8C83-1AE4239C19B1}.Release|x86.Build.0 = Release|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Debug|x64.Build.0 = Debug|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Debug|x86.Build.0 = Debug|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Release|Any CPU.Build.0 = Release|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Release|x64.ActiveCfg = Release|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Release|x64.Build.0 = Release|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Release|x86.ActiveCfg = Release|Any CPU + {A6B7B19C-3288-4CD2-A737-527BEB1ED807}.Release|x86.Build.0 = Release|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Debug|x64.Build.0 = Debug|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Debug|x86.Build.0 = Debug|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Release|Any CPU.Build.0 = Release|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Release|x64.ActiveCfg = Release|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Release|x64.Build.0 = Release|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Release|x86.ActiveCfg = Release|Any CPU + {AB501F63-8E8C-4333-8A15-81BA02F3C703}.Release|x86.Build.0 = Release|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Debug|x64.ActiveCfg = Debug|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Debug|x64.Build.0 = Debug|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Debug|x86.Build.0 = Debug|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Release|Any CPU.Build.0 = Release|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Release|x64.ActiveCfg = Release|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Release|x64.Build.0 = Release|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Release|x86.ActiveCfg = Release|Any CPU + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF}.Release|x86.Build.0 = Release|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Debug|Any CPU.Build.0 = Debug|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Debug|x64.ActiveCfg = Debug|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Debug|x64.Build.0 = Debug|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Debug|x86.ActiveCfg = Debug|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Debug|x86.Build.0 = Debug|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Release|Any CPU.ActiveCfg = Release|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Release|Any CPU.Build.0 = Release|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Release|x64.ActiveCfg = Release|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Release|x64.Build.0 = Release|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Release|x86.ActiveCfg = Release|Any CPU + {887E0794-798D-4127-847E-6F68D5C9B334}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -325,12 +401,22 @@ Global {8711D306-1118-4A11-9399-EF14AA13015E} = {540CEEC1-D541-4614-BF0B-18056A83E434} {8610485A-BE9F-4938-86D4-E9F1FA1739A0} = {F3664D7A-6FF5-4D1F-9F5F-26EE87F032D3} {0EA416B6-0AB6-464B-9F4D-206FFCFB262D} = {8F959801-D494-4CAF-9437-90F30472E169} + {8AE1015F-4D62-47EF-A576-2F7411EC20D5} = {F3664D7A-6FF5-4D1F-9F5F-26EE87F032D3} + {C505C63F-99E6-464F-8C83-1AE4239C19B1} = {8AE1015F-4D62-47EF-A576-2F7411EC20D5} + {A6B7B19C-3288-4CD2-A737-527BEB1ED807} = {8AE1015F-4D62-47EF-A576-2F7411EC20D5} + {AB501F63-8E8C-4333-8A15-81BA02F3C703} = {904D5094-55F9-4581-90AC-D6C1131F8152} + {6FDA7D49-DF44-4E8F-A182-0EB73DD3C452} = {904D5094-55F9-4581-90AC-D6C1131F8152} + {03B9C775-9582-409F-B67F-6B8A0CE0C7AF} = {904D5094-55F9-4581-90AC-D6C1131F8152} + {887E0794-798D-4127-847E-6F68D5C9B334} = {904D5094-55F9-4581-90AC-D6C1131F8152} + {904D5094-55F9-4581-90AC-D6C1131F8152} = {00FFDC13-7397-46F1-897E-A62A7575D28A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7433AE2-B1A0-4C1A-887E-5CAA7AAF67A6} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution + tests\Avalonia\Prism.Container.Avalonia.Shared\Prism.Container.Avalonia.Shared.projitems*{03b9c775-9582-409f-b67f-6b8a0ce0c7af}*SharedItemsImports = 5 tests\Wpf\Prism.Container.Wpf.Shared\Prism.Container.Wpf.Shared.projitems*{367be810-5b34-4894-be47-1c8dcc67de81}*SharedItemsImports = 5 + tests\Avalonia\Prism.Container.Avalonia.Shared\Prism.Container.Avalonia.Shared.projitems*{6fda7d49-df44-4e8f-a182-0eb73dd3c452}*SharedItemsImports = 13 tests\Wpf\Prism.Container.Wpf.Shared\Prism.Container.Wpf.Shared.projitems*{ba05687e-2317-4a65-805b-c596f52f7203}*SharedItemsImports = 5 tests\Wpf\Prism.Container.Wpf.Shared\Prism.Container.Wpf.Shared.projitems*{bd42a7d6-a84d-4d27-9c28-7f6a2ec477f1}*SharedItemsImports = 13 EndGlobalSection diff --git a/PrismLibrary_Avalonia.slnf b/PrismLibrary_Avalonia.slnf new file mode 100644 index 0000000000..4dc61b7f6e --- /dev/null +++ b/PrismLibrary_Avalonia.slnf @@ -0,0 +1,16 @@ +{ + "solution": { + "path": "PrismLibrary.sln", + "projects": [ + "src\\Avalonia\\Prism.Avalonia\\Prism.Avalonia.csproj", + "src\\Avalonia\\Prism.DryIoc.Avalonia\\Prism.DryIoc.Avalonia.csproj", + "src\\Prism.Core\\Prism.Core.csproj", + "src\\Prism.Events\\Prism.Events.csproj", + "tests\\Avalonia\\Prism.Avalonia.Tests\\Prism.Avalonia.Tests.csproj", + "tests\\Avalonia\\Prism.Container.Avalonia.Shared\\Prism.Container.Avalonia.Shared.shproj", + "tests\\Avalonia\\Prism.DryIoc.Avalonia.Tests\\Prism.DryIoc.Avalonia.Tests.csproj", + "tests\\Avalonia\\Prism.IocContainer.Avalonia.Tests.Support\\Prism.IocContainer.Avalonia.Tests.Support.csproj", + "tests\\Prism.Core.Tests\\Prism.Core.Tests.csproj" + ] + } +} \ No newline at end of file diff --git a/README.md b/README.md index b8edb84d63..de04d0354c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Prism -Prism is a framework for building loosely coupled, maintainable, and testable XAML applications in WPF, Xamarin Forms, Uno Platform and WinUI. Separate releases are available for each platform and those will be developed on independent timelines. Prism provides an implementation of a collection of design patterns that are helpful in writing well-structured and maintainable XAML applications, including MVVM, dependency injection, commands, EventAggregator, and others. Prism's core functionality is a shared code base supported in .NET Standard 2.0, .NET Framework 4.6 / 4.7, and .NET6.0/.NET8.0. Those things that need to be platform specific are implemented in the respective libraries for the target platform. Prism also provides great integration of these patterns with the target platform. +Prism is a framework for building loosely coupled, maintainable, and testable XAML applications in WPF, Avalonia, MAUI, Uno Platform and WinUI. Separate releases are available for each platform and those will be developed on independent timelines. Prism provides an implementation of a collection of design patterns that are helpful in writing well-structured and maintainable XAML applications, including MVVM, dependency injection, commands, EventAggregator, and others. Prism's core functionality is a shared code base supported in .NET Standard 2.0, .NET Framework 4.6 / 4.7, and .NET6.0/.NET8.0. Those things that need to be platform specific are implemented in the respective libraries for the target platform. Prism also provides great integration of these patterns with the target platform. ## Licensing @@ -13,7 +13,7 @@ The Prism Team would first and foremost like to thank all of those developers wh | Full Build | [![Prism CI](https://github.com/PrismLibrary/Prism/actions/workflows/ci.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/ci.yml) | | Prism.Core | [![build_core](https://github.com/PrismLibrary/Prism/actions/workflows/build_core.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/build_core.yml) | | Prism.Wpf | [![build_wpf](https://github.com/PrismLibrary/Prism/actions/workflows/build_wpf.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/build_wpf.yml) | -| Prism.Forms | [![build_forms](https://github.com/PrismLibrary/Prism/actions/workflows/build_forms.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/build_forms.yml) | +| Prism.Avalonia | [![build_avalonia](https://github.com/PrismLibrary/Prism/actions/workflows/build_avalonia.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/build_avalonia.yml) | | Prism.Uno | [![build_uno](https://github.com/PrismLibrary/Prism/actions/workflows/build_uno.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/build_uno.yml) | | Prism.Maui | [![build_maui](https://github.com/PrismLibrary/Prism/actions/workflows/build_maui.yml/badge.svg)](https://github.com/PrismLibrary/Prism/actions/workflows/build_maui.yml) | diff --git a/e2e/Avalonia/PrismAvaloniaDemo.sln b/e2e/Avalonia/PrismAvaloniaDemo.sln new file mode 100644 index 0000000000..903b5aa92c --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35122.118 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PrismAvaloniaDemo", "PrismAvaloniaDemo\PrismAvaloniaDemo.csproj", "{89EF9B20-B80E-4B4C-A202-170CC0C0B62A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Prism Library", "Prism Library", "{7EEF2BF7-98F5-432B-B36D-DB4915CF4A3D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Avalonia", "..\..\src\Avalonia\Prism.Avalonia\Prism.Avalonia.csproj", "{CD50461C-B394-433A-86CA-3DBCD13E8B22}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.DryIoc.Avalonia", "..\..\src\Avalonia\Prism.DryIoc.Avalonia\Prism.DryIoc.Avalonia.csproj", "{FF15E73E-5D71-48DD-80D5-DDCA6A59A6B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Core", "..\..\src\Prism.Core\Prism.Core.csproj", "{62ADE0CF-751D-4AC6-9C2C-21DC12CBD338}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prism.Events", "..\..\src\Prism.Events\Prism.Events.csproj", "{FE27B57B-F986-44B7-90CE-421034A891B9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {89EF9B20-B80E-4B4C-A202-170CC0C0B62A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89EF9B20-B80E-4B4C-A202-170CC0C0B62A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89EF9B20-B80E-4B4C-A202-170CC0C0B62A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89EF9B20-B80E-4B4C-A202-170CC0C0B62A}.Release|Any CPU.Build.0 = Release|Any CPU + {CD50461C-B394-433A-86CA-3DBCD13E8B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD50461C-B394-433A-86CA-3DBCD13E8B22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD50461C-B394-433A-86CA-3DBCD13E8B22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD50461C-B394-433A-86CA-3DBCD13E8B22}.Release|Any CPU.Build.0 = Release|Any CPU + {FF15E73E-5D71-48DD-80D5-DDCA6A59A6B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF15E73E-5D71-48DD-80D5-DDCA6A59A6B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF15E73E-5D71-48DD-80D5-DDCA6A59A6B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF15E73E-5D71-48DD-80D5-DDCA6A59A6B2}.Release|Any CPU.Build.0 = Release|Any CPU + {62ADE0CF-751D-4AC6-9C2C-21DC12CBD338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62ADE0CF-751D-4AC6-9C2C-21DC12CBD338}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62ADE0CF-751D-4AC6-9C2C-21DC12CBD338}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62ADE0CF-751D-4AC6-9C2C-21DC12CBD338}.Release|Any CPU.Build.0 = Release|Any CPU + {FE27B57B-F986-44B7-90CE-421034A891B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE27B57B-F986-44B7-90CE-421034A891B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE27B57B-F986-44B7-90CE-421034A891B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE27B57B-F986-44B7-90CE-421034A891B9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CD50461C-B394-433A-86CA-3DBCD13E8B22} = {7EEF2BF7-98F5-432B-B36D-DB4915CF4A3D} + {FF15E73E-5D71-48DD-80D5-DDCA6A59A6B2} = {7EEF2BF7-98F5-432B-B36D-DB4915CF4A3D} + {62ADE0CF-751D-4AC6-9C2C-21DC12CBD338} = {7EEF2BF7-98F5-432B-B36D-DB4915CF4A3D} + {FE27B57B-F986-44B7-90CE-421034A891B9} = {7EEF2BF7-98F5-432B-B36D-DB4915CF4A3D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E70249F-C31B-4FB3-A901-E3767E3A9FF5} + EndGlobalSection +EndGlobal diff --git a/e2e/Avalonia/PrismAvaloniaDemo/.editorconfig b/e2e/Avalonia/PrismAvaloniaDemo/.editorconfig new file mode 100644 index 0000000000..845700edbd --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/.editorconfig @@ -0,0 +1,378 @@ +# Copyright 2024 Xeno Innovations, Inc. +# +# This EditorConfig file provides consistent coding styles and formatting structure based on +# C# standards by Xeno Innovations for your team's projects while preserving your personal defaults. +# +# For more info: +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options +# + +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] +# All generic files should use MSDOS style endings, not Unix (lf) +# end_of_line = crlf +indent_style = space + +[*.{cs,csx}] +indent_style = space +indent_size = 4 +tab_width = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{xml,xaml,axml,axaml}] +indent_style = space +indent_size = 2 +charset = utf-8-bom +trim_trailing_whitespace = true + +[*.json] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.sln] +indent_size = 2 + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.svg] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +# PList Files +[*.plist] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +# YAML files +[*.{yaml,yml}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +# Shell script files +[*.sh] +end_of_line = lf +indent_style = space +indent_size = 2 + +# Powershell +[*.{ps1,psd1,psm1}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# C# Ruleset +[*.{cs,csx}] +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +## Formatting - new line options +### Require braces to be on a new line for (also known as "Allman" style) +### accessors, methods, object_collection, control_blocks, types, properties, lambdas +csharp_new_line_before_open_brace = all +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +## Spaces +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Organize Usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset +# file_header_template = Copyright Xeno Innovations, Inc. 2022\nSee the LICENSE file in the project root for more information. + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_require_accessibility_modifiers = for_non_interface_members +dotnet_style_readonly_field = true + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +#dotnet_diagnostic.IDE2000.severity = warning +dotnet_style_allow_multiple_blank_lines_experimental = false:error + +# dotnet_diagnostic.IDE2001.severity = none +csharp_style_allow_embedded_statements_on_same_line_experimental = false + +# dotnet_diagnostic.IDE2002.severity = warning +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false + +# dotnet_diagnostic.IDE2003.severity = error +dotnet_style_allow_statement_immediately_after_block_experimental = false:error + +# Naming Conventions +## Async methods must use "Async" suffix +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.severity = error +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.word_separator = + +## private fields must prefix with an underscore +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = error +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + +## private static fields must use PascalCase (overrides '_' based on SA1311) +dotnet_naming_rule.private_static_field_naming.symbols = private_static_field_naming +dotnet_naming_rule.private_static_field_naming.style = pascal_case_style +dotnet_naming_rule.private_static_field_naming.severity = error +dotnet_naming_symbols.private_static_field_naming.applicable_kinds = field +dotnet_naming_symbols.private_static_field_naming.applicable_accessibilities = private +dotnet_naming_symbols.private_static_field_naming.required_modifiers = static, readonly +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +## Constant fields must use PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +## Interfaces must have an I suffix +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.severity = error +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_style.begins_with_i.capitalization = pascal_case +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = + +## Types and Non-Field Members must be PascalCase +dotnet_naming_rule.types_should_be_pascal_case.severity = error +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +## Code Style Rules +# IDE1005: Use conditional delegate call +csharp_style_conditional_delegate_call = true +# IDE1005: Delegate invocation can be simplified. +dotnet_diagnostic.IDE1005.severity = warning +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = error +# IDEOOO8: Use of var +dotnet_diagnostic.IDE0008.severity = none +# IDE0010: Add missing cases +dotnet_diagnostic.IDE0010.severity = none +# IDE0011: Add braces +csharp_prefer_braces = when_multiline +# IDE0025: Use expression body for properties +csharp_style_expression_bodied_properties = true +# IDE0026: Use expression body for indexers +csharp_style_expression_bodied_indexers = true +# IDE0027: Use expression body for accessors +csharp_style_expression_bodied_accessors = true +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = none +# IDE0058: Expression value is never used +# csharp_style_unused_value_expression_statement_preference = discard_variable +dotnet_diagnostic.IDE0058.severity = none + +## Code Quality Rules +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none +# CA1822: Mark members as static +##dotnet_diagnostic.CA1822.severity = none +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = error + +## Compiler +# CS0618: Type or member is obsolete +##dotnet_diagnostic.CS0618.severity = error +dotnet_diagnostic.CS0618.severity = warning +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none + +## StyleCop.Analyzers +# SA1000: Keywords should be spaced correctly +dotnet_diagnostic.SA1000.severity = error +# SA1005: Single line comments should begin with single space +dotnet_diagnostic.SA1005.severity = error +# SA1008 +# SA1025: Spacing?? +# SA1101: PrefixLocalCallsWithThis +dotnet_diagnostic.SA1101.severity = none +# SA1116: Split parameters should start on line after declaration +dotnet_diagnostic.SA1116.severity = none +# SA1118: Parameter should not span multiple lines +dotnet_diagnostic.SA1118.severity = warning +# SA1137: Elements should have the same indentation +dotnet_diagnostic.SA1137.severity = error +# SA1124: Do not use regions +dotnet_diagnostic.SA1124.severity = error +# SA1200: UsingDirectivesMustBePlacedWithinNamespace +dotnet_diagnostic.SA1200.severity = none +# SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = error +# SA1202: Elements should be ordered by access +dotnet_diagnostic.SA1202.severity = error +# SA1203: Constants should appear before fields +dotnet_diagnostic.SA1203.severity = error +# SA1204: Static elements should appear before instance elements +dotnet_diagnostic.SA1204.severity = error +# SA1214: Readonly fields should appear before non-readonly fields +dotnet_diagnostic.SA1214.severity = error +# SA1306: Field names should begin with lower-case letter +dotnet_diagnostic.SA1306.severity = error +# SA1309: FieldNamesMustNotBeginWithUnderscore +dotnet_diagnostic.SA1309.severity = none +# SA1313: Parameter names should begin with lower-case letter +dotnet_diagnostic.SA1313.severity = error +# SA1414: Tuple types in signatures should have element names +dotnet_diagnostic.SA1414.severity = silent +# SA1503: Braces should not be omitted +dotnet_diagnostic.SA1503.severity = none +# SA1505: Opening braces should not be floowed by a blank line +dotnet_diagnostic.SA1505.severity= error +# SA1507: Code should not contain multiple blank lines in a row +dotnet_diagnostic.SA1507.severity = error +# SA1508: Closing brac should not be preceded by a blank line +dotnet_diagnostic.SA1508.severity= error +# SA1513: Closing brace should be followed by blank line +dotnet_diagnostic.SA1513.severity = error +# SA1514: Element documentation header should be preceded by blank line +dotnet_diagnostic.SA1514.severity = error +# SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = warning +# SA1516: Elements should be separated by blank line +dotnet_diagnostic.SA1516.severity = error +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none +# SA1602: Enumeration items should not be documented +dotnet_diagnostic.SA1602.severity = none +# SA1623: Property summary documentation should match accessors +dotnet_diagnostic.SA1623.severity = warning +# SA1633: FileMustHaveHeader +dotnet_diagnostic.SA1633.severity = silent +# SA1636: File header copyright text should match +dotnet_diagnostic.SA1636.severity = none + +# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.SpacingRules' SA1028: Code should not contain trailing whitespace +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = error + +dotnet_diagnostic.VSSpell001.severity = suggestion +dotnet_diagnostic.VSSpell002.severity = none + +# SA0001: XML comment analysis is disabled due to project configuration +dotnet_diagnostic.SA0001.severity = none + +# Ignore Generated XAML +[*.sg.cs] +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none +dotnet_analyzer_diagnostic.CS1591.severity = none +# generated_code = true + +[WindowsAppSDK-VersionInfo.cs] +dotnet_diagnostic.CS1591.severity = none diff --git a/e2e/Avalonia/PrismAvaloniaDemo/.gitignore b/e2e/Avalonia/PrismAvaloniaDemo/.gitignore new file mode 100644 index 0000000000..2ebe4cf80d --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/.gitignore @@ -0,0 +1,86 @@ +# Copyright CURRENT_YEAR COMPANY_NAME +# Template for C# + +# Generic Visual Studio files +*.bak +*.csproj.user +*.suo +*.vshost.exe +*.vshost.exe.manifest +*.exe.config +*.exp +*.lib +*.pdb +*.pfx +*.user +*.vbw +*.scc +*.oca +*.userprefs +*.userosscache +*.sln.docstates +*.autorecover +*.coverage + +# VS Code +.vscode/* +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json + +# SQLite datbases +*.db3 + +# Xamarin.Android Resource.Designer.cs files +**/*.Android/**/[Rr]esource.[Dd]esigner.cs +**/*.Droid/**/[Rr]esource.[Dd]esigner.cs +**/Android/**/[Rr]esource.[Dd]esigner.cs +**/Droid/**/[Rr]esource.[Dd]esigner.cs + +# Build results +[Oo]utput/ +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +.vs/ + +# NuGet Packages +*.nupkg +## The packages folder can be ignored because of Package Restore +**/packages/* +## Except build/, which is used as an MSBuild target. +!**/packages/build/ +## Uncomment if necessary however generally it will be regenerated when needed +!**/packages/repositories.config +## NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# NUnit +*.VisualState.xml +TestResult.xml + +# Installer Folder +/installer/*.exe +!/installer/autorun.exe + +# Lib folder +/lib/* +!/lib/readme.txt +!/lib/readme.md + +# Output +!/[Oo]utput/readme.txt +!/[Oo]utput/readme.md + +## USER DEFINED +/[Dd]ocs/*.csv +/[Dd]ocs/backup +/[Tt]ools diff --git a/e2e/Avalonia/PrismAvaloniaDemo/App.axaml b/e2e/Avalonia/PrismAvaloniaDemo/App.axaml new file mode 100644 index 0000000000..e662fa7e63 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/App.axaml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/e2e/Avalonia/PrismAvaloniaDemo/App.axaml.cs b/e2e/Avalonia/PrismAvaloniaDemo/App.axaml.cs new file mode 100644 index 0000000000..19fec496f4 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/App.axaml.cs @@ -0,0 +1,54 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using Prism.DryIoc; +using Prism.Ioc; +using Prism.Navigation.Regions; +using SampleApp.Services; +using SampleApp.ViewModels; +using SampleApp.Views; + +namespace SampleApp; + +public partial class App : PrismApplication +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + // Required when overriding Initialize + base.Initialize(); + } + + protected override AvaloniaObject CreateShell() + { + return Container.Resolve(); + } + + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + // Register your Services, Views, Dialogs, etc. here + + // Services + containerRegistry.RegisterSingleton(); + + // Views - Region Navigation + containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); + } + + /// Called after Initialize. + protected override void OnInitialized() + { + // Register Views to the Region it will appear in. Don't register them in the ViewModel. + var regionManager = Container.Resolve(); + + // WARNING: Prism v11.0.0 + // - DataTemplates MUST define a DataType or else an XAML error will be thrown + // - Error: DataTemplate inside of DataTemplates must have a DataType set + regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(DashboardView)); + } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Assets/logo.ico b/e2e/Avalonia/PrismAvaloniaDemo/Assets/logo.ico new file mode 100644 index 0000000000..e16d84d5d0 Binary files /dev/null and b/e2e/Avalonia/PrismAvaloniaDemo/Assets/logo.ico differ diff --git a/e2e/Avalonia/PrismAvaloniaDemo/PrismAvaloniaDemo.csproj b/e2e/Avalonia/PrismAvaloniaDemo/PrismAvaloniaDemo.csproj new file mode 100644 index 0000000000..b90410dc08 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/PrismAvaloniaDemo.csproj @@ -0,0 +1,34 @@ + + + WinExe + net8.0 + latest + enable + true + app.manifest + true + false + Sample Prism.Avalonia App + Damian Suess + Prism.Avalonia end-to-end demonstration application. + + + + + + + + + + + + + + + + + + + + + diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Program.cs b/e2e/Avalonia/PrismAvaloniaDemo/Program.cs new file mode 100644 index 0000000000..5e783c8451 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/Program.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia; + +namespace SampleApp; + +internal sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() => AppBuilder + .Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/RegionNames.cs b/e2e/Avalonia/PrismAvaloniaDemo/RegionNames.cs new file mode 100644 index 0000000000..7ebaa5ddde --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/RegionNames.cs @@ -0,0 +1,8 @@ +namespace SampleApp; + +public static class RegionNames +{ + public const string ContentRegion = "ContentRegion"; + public const string FooterRegion = "FooterRegion"; + public const string SidebarRegion = "SidebarRegion"; +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Services/INotificationService.cs b/e2e/Avalonia/PrismAvaloniaDemo/Services/INotificationService.cs new file mode 100644 index 0000000000..5c69f78cd9 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/Services/INotificationService.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; + +namespace SampleApp.Services; + +/// In-application Notification Service Interface. +public interface INotificationService +{ + /// Defines the maximum number of notifications visible at once. + int MaxItems { get; set; } + + /// The expiry time in seconds at which the notification will close (default 5 seconds). + int NotificationTimeout { get; set; } + + // Set the host window. + // Parent window. + void SetHostWindow(TopLevel window); + + /// Display the notification. + /// Title. + /// Message. + /// The of the notification. + /// An optional action to call when the notification is clicked. + /// An optional action to call when the notification is closed. + void Show(string title, + string message, + NotificationType notificationType = NotificationType.Information, + Action? onClick = null, + Action? onClose = null); +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Services/NotificationService.cs b/e2e/Avalonia/PrismAvaloniaDemo/Services/NotificationService.cs new file mode 100644 index 0000000000..7f6a009ea5 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/Services/NotificationService.cs @@ -0,0 +1,49 @@ +using System; +using Avalonia.Controls.Notifications; + +namespace SampleApp.Services; + +public class NotificationService : INotificationService +{ + private int _notificationTimeout = 5; + private WindowNotificationManager? _notificationManager; + + /// + public int MaxItems { get; set; } = 4; + + /// + public int NotificationTimeout { get => _notificationTimeout; set => _notificationTimeout = (value < 0) ? 0 : value; } + + /// + public void SetHostWindow(TopLevel hostWindow) + { + var notificationManager = new WindowNotificationManager(hostWindow) + { + Position = NotificationPosition.BottomRight, + MaxItems = MaxItems, + Margin = new Thickness(0, 0, 15, 40) + }; + + _notificationManager = notificationManager; + } + + /// + public void Show(string title, + string message, + NotificationType notificationType = NotificationType.Information, + Action? onClick = null, + Action? onClose = null) + { + if (_notificationManager is { } nm) + { + nm.Show( + new Notification( + title, + message, + notificationType, + TimeSpan.FromSeconds(_notificationTimeout), + onClick, + onClose)); + } + } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Styles/Icons.axaml b/e2e/Avalonia/PrismAvaloniaDemo/Styles/Icons.axaml new file mode 100644 index 0000000000..36a5d3b893 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/Styles/Icons.axaml @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/DashboardViewModel.cs b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000000..b9ae3ad215 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/DashboardViewModel.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Styling; +using Prism.Commands; +using SampleApp.Services; + +namespace SampleApp.ViewModels; + +public class DashboardViewModel : ViewModelBase +{ + private readonly INotificationService _notification; + private int _counter = 0; + private ObservableCollection _listItems = new(); + private int _listItemSelected = -1; + private string _listItemText = string.Empty; + private ThemeVariant? _themeSelected; + + public DashboardViewModel(INotificationService notifyService) + { + _notification = notifyService; + + ThemeSelected = Application.Current!.RequestedThemeVariant; + } + + public DelegateCommand CmdAddItem => new(() => + { + _counter++; + ListItems.Add($"Item Number: {_counter}"); + + // Optionally use, `Insert(0, ..)` to insert items at the top + //ListItems.Insert(0, entry); + }); + + public DelegateCommand CmdClearItems => new(() => + { + ListItems.Clear(); + }); + + public DelegateCommand CmdNotification => new(() => + { + _notification.Show("Hello Prism!", "Notification Pop-up Message."); + + // Alternate OnClick action + ////_notification.Show("Hello Prism!", "Notification Pop-up Message.", () => + ////{ + //// // Action to perform + ////}); + }); + + public ObservableCollection ListItems { get => _listItems; set => SetProperty(ref _listItems, value); } + + public int ListItemSelected + { + get => _listItemSelected; + set + { + SetProperty(ref _listItemSelected, value); + + if (value == -1) + return; + + ListItemText = ListItems[ListItemSelected]; + } + } + + public string ListItemText { get => _listItemText; set => SetProperty(ref _listItemText, value); } + + public ThemeVariant? ThemeSelected + { + get => _themeSelected; + set + { + SetProperty(ref _themeSelected, value); + Application.Current!.RequestedThemeVariant = _themeSelected; + } + } + + public List ThemeStyles => new() + { + ThemeVariant.Default, + ThemeVariant.Dark, + ThemeVariant.Light, + }; +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/MainWindowViewModel.cs b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..92a7af5aaf --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,41 @@ +using Prism.Commands; +using Prism.Navigation.Regions; + +using SampleApp.Views; + +namespace SampleApp.ViewModels; + +public class MainWindowViewModel : ViewModelBase +{ + private readonly IRegionManager _regionManager; + private bool _isPaneOpened; + + public MainWindowViewModel(IRegionManager regionManager) + { + // Since this is a basic ShellWindow, there's not much to do here. + // For enterprise apps, you could register up subscriptions + // or other startup background tasks so that they get triggered + // on startup, rather than putting them in the DashboardViewModel. + // + // For example, initiate the pulling of News Feeds, etc. + + _regionManager = regionManager; + Title = "Sample Prism.Avalonia"; + IsPaneOpened = true; + } + + public DelegateCommand CmdDashboard => new(() => + { + // _journal.Clear(); + _regionManager.RequestNavigate(RegionNames.ContentRegion, nameof(DashboardView)); + }); + + public DelegateCommand CmdFlyoutMenu => new(() => + { + IsPaneOpened = !IsPaneOpened; + }); + + public DelegateCommand CmdSettings => new(() => _regionManager.RequestNavigate(RegionNames.ContentRegion, nameof(SettingsView))); + + public bool IsPaneOpened { get => _isPaneOpened; set => SetProperty(ref _isPaneOpened, value); } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/SettingsViewModel.cs b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000000..9bbb327779 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/SettingsViewModel.cs @@ -0,0 +1,36 @@ +using SampleApp.Views; +using Prism.Commands; +using Prism.Navigation; +using Prism.Navigation.Regions; + +namespace SampleApp.ViewModels; + +public class SettingsViewModel : ViewModelBase +{ + private readonly IRegionManager _regionManager; + + public SettingsViewModel(IRegionManager regionManager) + { + _regionManager = regionManager; + Title = "Settings"; + } + + public DelegateCommand CmdNavigateToChild => new(() => + { + var navParams = new NavigationParameters + { + { "key1", "Some text" }, + { "key2", 999 } + }; + + _regionManager.RequestNavigate( + RegionNames.ContentRegion, + nameof(SubSettingsView), + navParams); + }); + + public override void OnNavigatedFrom(NavigationContext navigationContext) + { + base.OnNavigatedFrom(navigationContext); + } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/SubSettingsViewModel.cs b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/SubSettingsViewModel.cs new file mode 100644 index 0000000000..939bbe6cf2 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/SubSettingsViewModel.cs @@ -0,0 +1,53 @@ +using Prism.Commands; +using Prism.Navigation.Regions; +using SampleApp.Views; + +namespace SampleApp.ViewModels; + +public class SubSettingsViewModel : ViewModelBase +{ + private readonly IRegionManager _regionManager; + private IRegionNavigationJournal? _journal; + private string _messageNumber = string.Empty; + private string _messageText = string.Empty; + + public SubSettingsViewModel(IRegionManager regionManager) + { + _regionManager = regionManager; + + Title = "Settings - SubView"; + } + + public DelegateCommand CmdNavigateBack => new DelegateCommand(() => + { + // Go back to the previous calling page, otherwise, Dashboard. + if (_journal != null && _journal.CanGoBack) + _journal.GoBack(); + else + _regionManager.RequestNavigate(RegionNames.ContentRegion, nameof(DashboardView)); + }); + + public string MessageNumber { get => _messageNumber; set => SetProperty(ref _messageNumber, value); } + + public string MessageText { get => _messageText; set => SetProperty(ref _messageText, value); } + + /// Navigation completed successfully. + /// Navigation context. + public override void OnNavigatedTo(NavigationContext navigationContext) + { + // Used to "Go Back" to parent + _journal = navigationContext.NavigationService.Journal; + + // Display our parameters + MessageText = navigationContext.Parameters["key1"].ToString() ?? ""; + MessageNumber = navigationContext.Parameters["key2"].ToString() ?? ""; + } + + public override bool OnNavigatingTo(NavigationContext navigationContext) + { + // Navigation permission sample: + // Don't allow navigation if our keys are missing + return navigationContext.Parameters.ContainsKey("key1") && + navigationContext.Parameters.ContainsKey("key2"); + } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/ViewModelBase.cs b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000000..048a65f401 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/ViewModels/ViewModelBase.cs @@ -0,0 +1,45 @@ +using Prism.Mvvm; +using Prism.Navigation.Regions; + +namespace SampleApp.ViewModels; + +public class ViewModelBase : BindableBase, INavigationAware +{ + private string _title = string.Empty; + + /// Gets or sets the title of the View. + public string Title { get => _title; set => SetProperty(ref _title, value); } + + /// + /// Called to determine if this instance can handle the navigation request. + /// Don't call this directly, use . + /// + /// The navigation context. + /// if this instance accepts the navigation request; otherwise, . + public virtual bool IsNavigationTarget(NavigationContext navigationContext) + { + // Auto-allow navigation + return OnNavigatingTo(navigationContext); + } + + /// Called when the implementer is being navigated away from. + /// The navigation context. + public virtual void OnNavigatedFrom(NavigationContext navigationContext) + { + } + + /// Called when the implementer has been navigated to. + /// The navigation context. + public virtual void OnNavigatedTo(NavigationContext navigationContext) + { + } + + /// Navigation validation checker. + /// Override for Prism 7.2's IsNavigationTarget. + /// The navigation context. + /// if this instance accepts the navigation request; otherwise, . + public virtual bool OnNavigatingTo(NavigationContext navigationContext) + { + return true; + } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Views/DashboardView.axaml b/e2e/Avalonia/PrismAvaloniaDemo/Views/DashboardView.axaml new file mode 100644 index 0000000000..d9b8271eec --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/Views/DashboardView.axaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + diff --git a/e2e/Avalonia/PrismAvaloniaDemo/Views/SubSettingsView.axaml.cs b/e2e/Avalonia/PrismAvaloniaDemo/Views/SubSettingsView.axaml.cs new file mode 100644 index 0000000000..aa5c7558db --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/Views/SubSettingsView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace SampleApp.Views; + +public partial class SubSettingsView : UserControl +{ + public SubSettingsView() + { + InitializeComponent(); + } +} diff --git a/e2e/Avalonia/PrismAvaloniaDemo/app.manifest b/e2e/Avalonia/PrismAvaloniaDemo/app.manifest new file mode 100644 index 0000000000..dd6799f249 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/e2e/Avalonia/PrismAvaloniaDemo/settings.XamlStyler b/e2e/Avalonia/PrismAvaloniaDemo/settings.XamlStyler new file mode 100644 index 0000000000..170db90968 --- /dev/null +++ b/e2e/Avalonia/PrismAvaloniaDemo/settings.XamlStyler @@ -0,0 +1,44 @@ +{ + "AttributesTolerance": 2, + "KeepFirstAttributeOnSameLine": true, + "MaxAttributeCharactersPerLine": 0, + "MaxAttributesPerLine": 1, + "NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransfom, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter", + "SeparateByGroups": false, + "AttributeIndentation": 0, + "AttributeIndentationStyle": 1, + "RemoveDesignTimeReferences": false, + "EnableAttributeReordering": true, + "AttributeOrderingRuleGroups": [ + "xmlns, xmlns:x", + "xmlns:*", + "x:Class", + "x:*", + "*:*", + "Key, Name, Uid, Title, Text", + "Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom", + "Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight", + "Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex", + "Source", + "*", + "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", + "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText", + "Storyboard.*, From, To, Duration" + ], + "FirstLineAttributes": "Text", + "OrderAttributesByName": true, + "PutEndingBracketOnNewLine": false, + "RemoveEndingTagOfEmptyElement": true, + "SpaceBeforeClosingSlash": true, + "RootElementLineBreakRule": 0, + "ReorderVSM": 2, + "ReorderGridChildren": false, + "ReorderCanvasChildren": false, + "ReorderSetters": 0, + "FormatMarkupExtension": true, + "NoNewLineMarkupExtensions": "x:Bind, Binding", + "ThicknessSeparator": 2, + "ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin", + "FormatOnSave": true, + "CommentPadding": 1 +} \ No newline at end of file diff --git a/images/Prism.Avalonia.png b/images/Prism.Avalonia.png new file mode 100644 index 0000000000..1d1c114966 Binary files /dev/null and b/images/Prism.Avalonia.png differ diff --git a/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs b/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs new file mode 100644 index 0000000000..721b92f907 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs @@ -0,0 +1,51 @@ +using System.ComponentModel; +using Prism.Extensions; +using Avalonia; +using Avalonia.Controls; + +namespace Prism.Common +{ + /// + /// Class that wraps an object, so that other classes can notify for Change events. Typically, this class is set as + /// a Dependency Property on AvaloniaObjects, and allows other classes to observe any changes in the Value. + /// + /// + /// This class is required, because in Silverlight, it's not possible to receive Change notifications for Dependency properties that you do not own. + /// + /// The type of the property that's wrapped in the Observable object + public class ObservableObject : Control, INotifyPropertyChanged + { + /// + /// Identifies the Value property of the ObservableObject + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "This is the pattern for dependency properties")] + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(name: nameof(Value)); + + /// + /// Event that gets invoked when the Value property changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// The value that's wrapped inside the ObservableObject. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")] + public T Value + { + get => (T)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + private static void ValueChangedCallback(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + ObservableObject thisInstance = ((ObservableObject)d); + thisInstance.PropertyChanged?.Invoke(thisInstance, new PropertyChangedEventArgs(nameof(Value))); + } + + static ObservableObject() + { + ValueProperty.Changed.Subscribe(args => ValueChangedCallback(args?.Sender, args)); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Common/Stubs.cs b/src/Avalonia/Prism.Avalonia/Common/Stubs.cs new file mode 100644 index 0000000000..bbc331eeda --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Common/Stubs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Prism.Common +{ + internal static class Stubs + { + public static readonly Action Nop = () => { }; + public static readonly Action Throw = ex => { throw ex; }; + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs b/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs new file mode 100644 index 0000000000..84642492a6 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs @@ -0,0 +1,75 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Styling; +using Prism.Extensions; + +namespace Prism.Dialogs +{ + /// This class contains attached properties. + public class Dialog + { + /// Identifies the WindowStyle attached property. + /// This attached property is used to specify the style of a . + public static readonly AvaloniaProperty WindowStyleProperty = + AvaloniaProperty.RegisterAttached("WindowStyle", typeof(Dialog)); + + /// Identifies the WindowStartupLocation attached property. + /// This attached property is used to specify the startup location of a . + public static readonly AvaloniaProperty WindowStartupLocationProperty = + AvaloniaProperty.RegisterAttached( + name: "WindowStartupLocation", + ownerType: typeof(Dialog)); + + /// Creates an instance of the Dialog class. + public Dialog() + { + WindowStartupLocationProperty.Changed.Subscribe(args => OnWindowStartupLocationChanged(args?.Sender, args)); + } + + /// + /// Gets the value for the attached property. + /// + /// The target element. + /// The attached to the element. + public static Style GetWindowStyle(AvaloniaObject obj) + { + return (Style)obj.GetValue(WindowStyleProperty); + } + + /// + /// Sets the attached property. + /// + /// The target element. + /// The Style to attach. + public static void SetWindowStyle(AvaloniaObject obj, Style value) + { + obj.SetValue(WindowStyleProperty, value); + } + + /// + /// Gets the value for the attached property. + /// + /// The target element. + /// The attached to the element. + public static WindowStartupLocation GetWindowStartupLocation(AvaloniaObject obj) + { + return (WindowStartupLocation)obj.GetValue(WindowStartupLocationProperty); + } + + /// + /// Sets the attached property. + /// + /// The target element. + /// The WindowStartupLocation to attach. + public static void SetWindowStartupLocation(AvaloniaObject obj, WindowStartupLocation value) + { + obj.SetValue(WindowStartupLocationProperty, value); + } + + private static void OnWindowStartupLocationChanged(AvaloniaObject sender, AvaloniaPropertyChangedEventArgs e) + { + if (sender is Window window) + window.WindowStartupLocation = (WindowStartupLocation)e.NewValue; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs b/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs new file mode 100644 index 0000000000..f091944c5e --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs @@ -0,0 +1,180 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Prism.Common; +using Prism.Ioc; + +namespace Prism.Dialogs +{ + /// Implements to show modal and non-modal dialogs. + /// The dialog's ViewModel must implement IDialogAware. + public class DialogService : IDialogService + { + private readonly IContainerExtension _containerExtension; + + /// Initializes a new instance of the class. + /// The + public DialogService(IContainerExtension containerExtension) + { + _containerExtension = containerExtension; + } + + /// Show dialog. + /// Name of the dialog window to show. + /// . + /// The action to perform when the dialog is closed. + public void ShowDialog(string name, IDialogParameters parameters, DialogCallback callback) + { + parameters ??= new DialogParameters(); + var isModal = parameters.TryGetValue(KnownDialogParameters.ShowNonModal, out var show) ? !show : true; + var windowName = parameters.TryGetValue(KnownDialogParameters.WindowName, out var wName) ? wName : null; + var owner = parameters.TryGetValue(KnownDialogParameters.ParentWindow, out var hWnd) ? hWnd : null; + + IDialogWindow dialogWindow = CreateDialogWindow(windowName); + ConfigureDialogWindowEvents(dialogWindow, callback); + ConfigureDialogWindowContent(name, dialogWindow, parameters); + + ShowDialogWindow(dialogWindow, isModal, owner); + } + + /// Shows the dialog window. + /// The dialog window to show. + /// If true; dialog is shown as a modal + /// Optional host window of the dialog. Use-case, Dialog calling a dialog. + protected virtual void ShowDialogWindow(IDialogWindow dialogWindow, bool isModal, Window owner = null) + { + if (isModal && + Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime deskLifetime) + { + // Ref: + // - https://docs.avaloniaui.net/docs/reference/controls/window#show-a-window-as-a-dialog + // - https://github.com/AvaloniaUI/Avalonia/discussions/7924 + // (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + + if (owner != null) + dialogWindow.ShowDialog(owner); + else + dialogWindow.ShowDialog(deskLifetime.MainWindow); + } + else + { + dialogWindow.Show(); + } + } + + /// + /// Create a new . + /// + /// The name of the hosting window registered with the IContainerRegistry. + /// The created . + protected virtual IDialogWindow CreateDialogWindow(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return _containerExtension.Resolve(); + else + return _containerExtension.Resolve(name); + } + + /// + /// Configure content. + /// + /// The name of the dialog to show. + /// The hosting window. + /// The parameters to pass to the dialog. + protected virtual void ConfigureDialogWindowContent(string dialogName, IDialogWindow window, IDialogParameters parameters) + { + var content = _containerExtension.Resolve(dialogName); + if (!(content is Avalonia.Controls.Control dialogContent)) + throw new NullReferenceException("A dialog's content must be an Avalonia.Controls.Control"); + + MvvmHelpers.AutowireViewModel(dialogContent); + + if (!(dialogContent.DataContext is IDialogAware viewModel)) + throw new NullReferenceException("A dialog's ViewModel must implement the IDialogAware interface"); + + ConfigureDialogWindowProperties(window, dialogContent, viewModel); + + MvvmHelpers.ViewAndViewModelAction(viewModel, d => d.OnDialogOpened(parameters)); + } + + /// + /// Configure and events. + /// + /// The hosting window. + /// The action to perform when the dialog is closed. + protected virtual void ConfigureDialogWindowEvents(IDialogWindow dialogWindow, DialogCallback callback) + { + Action requestCloseHandler = (result) => + { + dialogWindow.Result = result; + dialogWindow.Close(); + }; + + EventHandler loadedHandler = null; + + loadedHandler = (o, e) => + { + // WPF: dialogWindow.Loaded -= loadedHandler; + dialogWindow.Opened -= loadedHandler; + DialogUtilities.InitializeListener(dialogWindow.GetDialogViewModel(), requestCloseHandler); + }; + + dialogWindow.Opened += loadedHandler; + + EventHandler closingHandler = null; + closingHandler = (o, e) => + { + if (!dialogWindow.GetDialogViewModel().CanCloseDialog()) + e.Cancel = true; + }; + + dialogWindow.Closing += closingHandler; + + EventHandler closedHandler = null; + closedHandler = async (o, e) => + { + dialogWindow.Closed -= closedHandler; + dialogWindow.Closing -= closingHandler; + + dialogWindow.GetDialogViewModel().OnDialogClosed(); + + if (dialogWindow.Result == null) + dialogWindow.Result = new DialogResult(); + + await callback.Invoke(dialogWindow.Result); + + dialogWindow.DataContext = null; + dialogWindow.Content = null; + }; + + dialogWindow.Closed += closedHandler; + } + + /// + /// Configure properties. + /// + /// The hosting window. + /// The dialog to show. + /// The dialog's ViewModel. + protected virtual void ConfigureDialogWindowProperties(IDialogWindow window, Avalonia.Controls.Control dialogContent, IDialogAware viewModel) + { + // Avalonia returns 'null' for Dialog.GetWindowStyle(dialogContent); + // WPF: Window > ContentControl > FrameworkElement + // Ava: Window > WindowBase > TopLevel > Control > InputElement > Interactive > Layoutable > Visual > StyledElement.Styles (collection) + + // WPF: + //// var windowStyle = Dialog.GetWindowStyle(dialogContent); + //// if (windowStyle != null) + //// window.Style = windowStyle; + + // Make the host window and the dialog window to share the same context + window.Content = dialogContent; + window.DataContext = viewModel; + + // WPF: + //// if (window.Owner == null) + //// window.Owner = Application.Current?.Windows.OfType().FirstOrDefault(x => x.IsActive); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml new file mode 100644 index 0000000000..ee77baa2d6 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs new file mode 100644 index 0000000000..e486c41d22 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Prism.Dialogs +{ + /// Prism's default dialog host. + public partial class DialogWindow : Window, IDialogWindow + { + /// The of the dialog. + public IDialogResult Result { get; set; } + + /// Initializes a new instance of the class. + public DialogWindow() + { + InitializeComponent(); + +#if DEBUG + //// this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs new file mode 100644 index 0000000000..7e9b5f97f9 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia.Controls; + +namespace Prism.Dialogs +{ + /// Extensions for the IDialogService + public static class IDialogServiceCompatExtensions + { + /// Shows a non-modal dialog. + /// The DialogService + /// The name of the dialog to show. + /// Parameters that the dialog can use for custom functionality. + /// The action to be invoked upon successful or failed completion of displaying the dialog. + public static void Show(this IDialogService dialogService, string name, IDialogParameters parameters, Action callback) + { + parameters = EnsureShowNonModalParameter(parameters); + dialogService.ShowDialog(name, parameters, new DialogCallback().OnClose(callback)); + } + + /// Shows a non-modal dialog. + /// The DialogService + /// The name of the dialog to show. + /// The parameters to pass to the dialog. + /// The action to perform when the dialog is closed. + /// The name of the hosting window registered with the IContainerRegistry. + public static void Show(this IDialogService dialogService, string name, IDialogParameters parameters, Action callback, string windowName) + { + parameters = EnsureShowNonModalParameter(parameters); + + if (!string.IsNullOrEmpty(windowName)) + parameters.Add(KnownDialogParameters.WindowName, windowName); + + dialogService.ShowDialog(name, parameters, new DialogCallback().OnClose(callback)); + } + + /// Shows a non-modal dialog. + /// The DialogService + /// The name of the dialog to show. + public static void Show(this IDialogService dialogService, string name) + { + var parameters = EnsureShowNonModalParameter(null); + dialogService.Show(name, parameters, null); + } + + /// Shows a non-modal dialog. + /// The DialogService + /// The name of the dialog to show. + /// The action to perform when the dialog is closed. + public static void Show(this IDialogService dialogService, string name, Action callback) + { + var parameters = EnsureShowNonModalParameter(null); + dialogService.Show(name, parameters, callback); + } + + private static IDialogParameters EnsureShowNonModalParameter(IDialogParameters parameters) + { + parameters ??= new DialogParameters(); + + if (!parameters.ContainsKey(KnownDialogParameters.ShowNonModal)) + parameters.Add(KnownDialogParameters.ShowNonModal, true); + + return parameters; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs new file mode 100644 index 0000000000..71557adb08 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Styling; + +#nullable enable +namespace Prism.Dialogs +{ + /// + /// Interface for a dialog hosting window. + /// + public interface IDialogWindow + { + /// Dialog content. + object Content { get; set; } + + /// Close the window. + void Close(); + + /// The window's owner. + /// Avalonia's WindowBase.Owner's property access is { get; protected set; }. + WindowBase Owner { get; } + + /// Show a non-modal dialog. + void Show(); + + /// Show a modal dialog. + /// Task. + Task ShowDialog(Window owner); + + /// The data context of the window. + /// The data context must implement . + object DataContext { get; set; } + + /// Called when the window is loaded. + /// + /// WPF: event RoutedEventHandler Loaded; + /// Avalonia currently doesn't implement the Loaded event like WPF. + /// Window > WindowBase > TopLevel.Opened + /// Window > WindowBase > TopLevel > Control > InputElement > Interactive > layout > Visual > StyledElement.Initialized + /// + event EventHandler Opened; + + /// + /// Called when the window is closed. + /// + event EventHandler Closed; + + /// Called when the window is closing. + event EventHandler? Closing; + + /// The result of the dialog. + IDialogResult Result { get; set; } + + /////// The window style. + /////// + /////// WPF: Window > ContentControl > FrameworkElement + /////// Ava: Window > WindowBase > TopLevel > ContentControl > TemplatedControl > Control + /////// + ////Style Style { get; set; } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs new file mode 100644 index 0000000000..d7708cd20d --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs @@ -0,0 +1,18 @@ +namespace Prism.Dialogs +{ + /// + /// extensions. + /// + internal static class IDialogWindowExtensions + { + /// + /// Get the ViewModel from a . + /// + /// to get ViewModel from. + /// ViewModel as a . + internal static IDialogAware GetDialogViewModel(this IDialogWindow dialogWindow) + { + return (IDialogAware)dialogWindow.DataContext; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs b/src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs new file mode 100644 index 0000000000..df0ceb7023 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs @@ -0,0 +1,14 @@ +namespace Prism.Dialogs; + +/// Provides Dialog Parameter Keys for well known parameters used by the +public static class KnownDialogParameters +{ + /// The name of the window. + public const string WindowName = "windowName"; + + /// Flag to show the Dialog Modally or Non-Modally. + public const string ShowNonModal = "nonModal"; + + /// Host Window; when different from default. + public const string ParentWindow = "parentWindow"; +} diff --git a/src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs b/src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs new file mode 100644 index 0000000000..92b7001218 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Controls; + +namespace Prism +{ + /// AvaloniaObject Extensions. + /// Equivalent to WPF's DependencyObject + internal static partial class AvaloniaObjectExtensions + { + /// Determines if a has a binding set. + /// The to use to search for the property. + /// The property to search. + /// true if there is an active binding, otherwise false. + public static bool HasBinding(this Control instance, AvaloniaProperty property) + => instance.GetBindingObservable(property) != null; + } +} diff --git a/src/Avalonia/Prism.Avalonia/Extensions/ObservableExtensions.cs b/src/Avalonia/Prism.Avalonia/Extensions/ObservableExtensions.cs new file mode 100644 index 0000000000..ccf763cfc9 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Extensions/ObservableExtensions.cs @@ -0,0 +1,35 @@ +using System; +using Avalonia.Reactive; +using Prism.Common; + +namespace Prism.Extensions +{ + internal static class ObservableExtensions + { + /// + /// Subscribes an element handler to an observable sequence. + /// + /// The type of the elements in the source sequence. + /// Observable sequence to subscribe to. + /// Action to invoke for each element in the observable sequence. + /// object used to unsubscribe from the observable sequence. + /// or is null. + public static IDisposable Subscribe(this IObservable source, Action onNext) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + // + // [OK] Use of unsafe Subscribe: non-pretentious constructor for an observer; this overload is not to be used internally. + // + return source.Subscribe/*Unsafe*/(new AnonymousObserver(onNext, Stubs.Throw, Stubs.Nop)); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs b/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs new file mode 100644 index 0000000000..3d835882de --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs @@ -0,0 +1,228 @@ +// TODO - 2022-07-12 +// * Updated the public StyleProperty fields for Avalonia +// * Needs: +// - Methods updated and verified - OnAttached, OnDetatching, , etc. +// +// Reference: +// https://github.com/wieslawsoltes/AvaloniaBehaviors/blob/master/src/Avalonia.Xaml.Interactions/Core/InvokeCommandAction.cs +// +/* +using System.Reflection; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Microsoft.Xaml.Behaviors; + +namespace Prism.Interactivity +{ + /// + /// Trigger action that executes a command when invoked. + /// It also maintains the Enabled state of the target control based on the CanExecute method of the command. + /// + public class InvokeCommandAction : TriggerAction + { + private ExecutableCommandBehavior _commandBehavior; + + /// + /// Dependency property identifying if the associated element should automatically be enabled or disabled based on the result of the Command's CanExecute + /// + public static readonly StyledProperty AutoEnableProperty = + AvaloniaProperty.Register(nameof(AutoEnable)); + ////public static readonly StyledProperty AutoEnableProperty = + //// StyledProperty.Register("AutoEnable", typeof(bool), typeof(InvokeCommandAction), + //// new PropertyMetadata(true, (d, e) => ((InvokeCommandAction)d).OnAllowDisableChanged((bool)e.NewValue))); + + /// + /// Gets or sets whether or not the associated element will automatically be enabled or disabled based on the result of the commands CanExecute + /// + public bool AutoEnable + { + get { return (bool)this.GetValue(AutoEnableProperty); } + set { this.SetValue(AutoEnableProperty, value); } + } + + private void OnAllowDisableChanged(bool newValue) + { + var behavior = GetOrCreateBehavior(); + if (behavior != null) + behavior.AutoEnable = newValue; + } + + /// + /// Dependency property identifying the command to execute when invoked. + /// + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command)); + ////public static readonly StyledProperty CommandProperty = + //// StyledProperty.Register("Command", typeof(ICommand), typeof(InvokeCommandAction), + //// new PropertyMetadata(null, (d, e) => ((InvokeCommandAction)d).OnCommandChanged((ICommand)e.NewValue))); + + /// + /// Gets or sets the command to execute when invoked. + /// + public ICommand Command + { + get { return this.GetValue(CommandProperty) as ICommand; } + set { this.SetValue(CommandProperty, value); } + } + + private void OnCommandChanged(ICommand newValue) + { + var behavior = GetOrCreateBehavior(); + if (behavior != null) + behavior.Command = newValue; + } + + /// + /// Dependency property identifying the command parameter to supply on command execution. + /// + public static readonly StyledProperty CommandParameterProperty = + AvaloniaProperty.Register(nameof(CommandParameter)); + ////public static readonly StyledProperty CommandParameterProperty = + //// StyledProperty.Register("CommandParameter", typeof(object), typeof(InvokeCommandAction), + //// new PropertyMetadata(null, (d, e) => ((InvokeCommandAction)d).OnCommandParameterChanged(e.NewValue))); + + /// + /// Gets or sets the command parameter to supply on command execution. + /// + public object CommandParameter + { + get { return this.GetValue(CommandParameterProperty); } + set { this.SetValue(CommandParameterProperty, value); } + } + + private void OnCommandParameterChanged(object newValue) + { + var behavior = GetOrCreateBehavior(); + if (behavior != null) + behavior.CommandParameter = newValue; + } + + /// + /// Dependency property identifying the TriggerParameterPath to be parsed to identify the child property of the trigger parameter to be used as the command parameter. + /// + public static readonly StyledProperty TriggerParameterPathProperty = + AvaloniaProperty.Register(nameof(TriggerParameterPath)); + ////public static readonly StyledProperty TriggerParameterPathProperty = + //// StyledProperty.Register("TriggerParameterPath", typeof(string), typeof(InvokeCommandAction), + //// new PropertyMetadata(null, (d, e) => { })); + + /// + /// Gets or sets the TriggerParameterPath value. + /// + public string TriggerParameterPath + { + get { return this.GetValue(TriggerParameterPathProperty) as string; } + set { this.SetValue(TriggerParameterPathProperty, value); } + } + + /// + /// Public wrapper of the Invoke method. + /// + public void InvokeAction(object parameter) + { + Invoke(parameter); + } + + /// + /// Executes the command + /// + /// This parameter is passed to the command; the CommandParameter specified in the CommandParameterProperty is used for command invocation if not null. + protected override void Invoke(object parameter) + { + if (!string.IsNullOrEmpty(TriggerParameterPath)) + { + //Walk the ParameterPath for nested properties. + var propertyPathParts = TriggerParameterPath.Split('.'); + object propertyValue = parameter; + foreach (var propertyPathPart in propertyPathParts) + { + var propInfo = propertyValue.GetType().GetTypeInfo().GetProperty(propertyPathPart); + propertyValue = propInfo.GetValue(propertyValue); + } + parameter = propertyValue; + } + + var behavior = GetOrCreateBehavior(); + + if (behavior != null) + { + behavior.ExecuteCommand(parameter); + } + } + + /// + /// Sets the Command and CommandParameter properties to null. + /// + protected override void OnDetaching() + { + base.OnDetaching(); + + Command = null; + CommandParameter = null; + + _commandBehavior = null; + } + + /// + /// This method is called after the behavior is attached. + /// It updates the command behavior's Command and CommandParameter properties if necessary. + /// + protected override void OnAttached() + { + base.OnAttached(); + + // In case this action is attached to a target object after the Command and/or CommandParameter properties are set, + // the command behavior would be created without a value for these properties. + // To cover this scenario, the Command and CommandParameter properties of the behavior are updated here. + var behavior = GetOrCreateBehavior(); + + behavior.AutoEnable = AutoEnable; + + if (behavior.Command != Command) + behavior.Command = Command; + + if (behavior.CommandParameter != CommandParameter) + behavior.CommandParameter = CommandParameter; + } + + private ExecutableCommandBehavior GetOrCreateBehavior() + { + // In case this method is called prior to this action being attached, + // the CommandBehavior would always keep a null target object (which isn't changeable afterwards). + // Therefore, in that case the behavior shouldn't be created and this method should return null. + if (_commandBehavior == null && AssociatedObject != null) + { + _commandBehavior = new ExecutableCommandBehavior(AssociatedObject); + } + + return _commandBehavior; + } + + /// + /// A CommandBehavior that exposes a public ExecuteCommand method. It provides the functionality to invoke commands and update Enabled state of the target control. + /// It is not possible to make the inherit from , since the + /// must already inherit from , so we chose to follow the aggregation approach. + /// + private class ExecutableCommandBehavior : CommandBehaviorBase + { + /// + /// Constructor specifying the target object. + /// + /// The target object the behavior is attached to. + public ExecutableCommandBehavior(Control target) + : base(target) + { + } + + /// + /// Executes the command, if it's set. + /// + public new void ExecuteCommand(object parameter) + { + base.ExecuteCommand(parameter); + } + } + } +} +*/ diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs new file mode 100644 index 0000000000..dd473e12aa --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs @@ -0,0 +1,103 @@ +using Avalonia; +using Prism.Common; +using System.Collections; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Defines a behavior that forwards the + /// to the views in the region. + /// + public class BindRegionContextToAvaloniaObjectBehavior : IRegionBehavior + { + + /// The key of this behavior. + /// (2024-04-11_Suess): This SHOULD be ''ContextToAvaloniaObject'. + public const string BehaviorKey = "ContextToDependencyObject"; + + /// Behavior's attached region. + public IRegion Region { get; set; } + + /// Attaches the behavior to the specified region. + public void Attach() + { + Region.Views.CollectionChanged += Views_CollectionChanged; + Region.PropertyChanged += Region_PropertyChanged; + SetContextToViews(Region.Views, Region.Context); + AttachNotifyChangeEvent(Region.Views); + } + + private static void SetContextToViews(IEnumerable views, object context) + { + foreach (var view in views) + { + AvaloniaObject avaloniaObjectView = view as AvaloniaObject; + if (avaloniaObjectView != null) + { + ObservableObject contextWrapper = RegionContext.GetObservableContext(avaloniaObjectView); + contextWrapper.Value = context; + } + } + } + + private void AttachNotifyChangeEvent(IEnumerable views) + { + foreach (var view in views) + { + var avaloniaObject = view as AvaloniaObject; + if (avaloniaObject != null) + { + ObservableObject viewRegionContext = RegionContext.GetObservableContext(avaloniaObject); + viewRegionContext.PropertyChanged += ViewRegionContext_OnPropertyChangedEvent; + } + } + } + + private void DetachNotifyChangeEvent(IEnumerable views) + { + foreach (var view in views) + { + var avaloniaObject = view as AvaloniaObject; + if (avaloniaObject != null) + { + ObservableObject viewRegionContext = RegionContext.GetObservableContext(avaloniaObject); + viewRegionContext.PropertyChanged -= ViewRegionContext_OnPropertyChangedEvent; + } + } + } + + private void ViewRegionContext_OnPropertyChangedEvent(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == "Value") + { + var context = (ObservableObject)sender; + Region.Context = context.Value; + } + } + + private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + SetContextToViews(e.NewItems, Region.Context); + AttachNotifyChangeEvent(e.NewItems); + } + else if (e.Action == NotifyCollectionChangedAction.Remove && Region.Context != null) + { + DetachNotifyChangeEvent(e.OldItems); + SetContextToViews(e.OldItems, null); + + } + } + + private void Region_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Context") + { + SetContextToViews(Region.Views, Region.Context); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs new file mode 100644 index 0000000000..dc99126fe6 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs @@ -0,0 +1,191 @@ +// TODO: 2022-07-08 - Feature disabled until a workaround can be created +// Consider using Avalonia.Styling.IStylable or Avalonia.Styling.Selector +// in place of WPF's `Selector` object or AvaloniaObject. +// This theory is untested and causes issues on code such as, `hostControl.Items` +// - private Selector hostControl; +// - public IStyleable HostControl +// Ref: +// - https://github.com/AvaloniaUI/Avalonia/issues/3593 +// - https://stackoverflow.com/questions/44241761/how-do-style-selectors-work-in-avalonia +// - https://stackoverflow.com/questions/72238118/avalonia-style-selector-doent-works-on-derived-classes +/* +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Prism.Properties; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Controls.Primitives; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Defines the attached behavior that keeps the items of the host control in synchronization with the . + /// + /// This behavior also makes sure that, if you activate a view in a region, the SelectedItem is set. If you set the SelectedItem or SelectedItems (ListBox) + /// then this behavior will also call Activate on the selected items. + /// + /// When calling Activate on a view, you can only select a single active view at a time. By setting the SelectedItems property of a listbox, you can set + /// multiple views to active. + /// + /// + public class SelectorItemsSourceSyncBehavior : RegionBehavior, IHostAwareRegionBehavior + { + /// + /// Name that identifies the SelectorItemsSourceSyncBehavior behavior in a collection of RegionsBehaviors. + /// + public static readonly string BehaviorKey = "SelectorItemsSourceSyncBehavior"; + private bool updatingActiveViewsInHostControlSelectionChanged; + private Selector hostControl; + + /// + /// Gets or sets the that the is attached to. + /// + /// + /// A that the is attached to. + /// + /// For this behavior, the host control must always be a or an inherited class. + public AvaloniaObject HostControl + { + get + { + return this.hostControl; + } + + set + { + this.hostControl = value as Selector; + } + } + + /// + /// Starts to monitor the to keep it in synch with the items of the . + /// + protected override void OnAttach() + { + bool itemsSourceIsSet = this.hostControl.ItemsSource != null; + itemsSourceIsSet = itemsSourceIsSet || (hostControl.HasBinding(this.hostControl, ItemsControl.ItemsSourceProperty) != null); + ////itemsSourceIsSet = itemsSourceIsSet || (BindingOperations.GetBinding(this.hostControl, ItemsControl.ItemsSourceProperty) != null); + + if (itemsSourceIsSet) + { + throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException); + } + + this.SynchronizeItems(); + + this.hostControl.SelectionChanged += this.HostControlSelectionChanged; + this.Region.ActiveViews.CollectionChanged += this.ActiveViews_CollectionChanged; + this.Region.Views.CollectionChanged += this.Views_CollectionChanged; + } + + private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + int startIndex = e.NewStartingIndex; + foreach (object newItem in e.NewItems) + { + this.hostControl.Items.Insert(startIndex++, newItem); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (object oldItem in e.OldItems) + { + this.hostControl.Items.Remove(oldItem); + } + } + } + + private void SynchronizeItems() + { + List existingItems = new List(); + + // Control must be empty before "Binding" to a region + foreach (object childItem in this.hostControl.Items) + { + existingItems.Add(childItem); + } + + foreach (object view in this.Region.Views) + { + this.hostControl.Items.Add(view); + } + + foreach (object existingItem in existingItems) + { + this.Region.Add(existingItem); + } + } + + + private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (this.updatingActiveViewsInHostControlSelectionChanged) + { + // If we are updating the ActiveViews collection in the HostControlSelectionChanged, that + // means the user has set the SelectedItem or SelectedItems himself and we don't need to do that here now + return; + } + + if (e.Action == NotifyCollectionChangedAction.Add) + { + if (this.hostControl.SelectedItem != null + && this.hostControl.SelectedItem != e.NewItems[0] + && this.Region.ActiveViews.Contains(this.hostControl.SelectedItem)) + { + this.Region.Deactivate(this.hostControl.SelectedItem); + } + + this.hostControl.SelectedItem = e.NewItems[0]; + } + else if (e.Action == NotifyCollectionChangedAction.Remove && + e.OldItems.Contains(this.hostControl.SelectedItem)) + { + this.hostControl.SelectedItem = null; + } + } + + private void HostControlSelectionChanged(object sender, SelectionChangedEventArgs e) + { + try + { + // Record the fact that we are now updating active views in the HostControlSelectionChanged method. + // This is needed to prevent the ActiveViews_CollectionChanged() method from firing. + this.updatingActiveViewsInHostControlSelectionChanged = true; + + object source; + source = e.Source; // source = e.OriginalSource; + + if (source == sender) + { + foreach (object item in e.RemovedItems) + { + // check if the view is in both Views and ActiveViews collections (there may be out of sync) + if (this.Region.Views.Contains(item) && this.Region.ActiveViews.Contains(item)) + { + this.Region.Deactivate(item); + } + } + + foreach (object item in e.AddedItems) + { + if (this.Region.Views.Contains(item) && !this.Region.ActiveViews.Contains(item)) + { + this.Region.Activate(item); + } + } + } + } + finally + { + this.updatingActiveViewsInHostControlSelectionChanged = false; + } + } + } +} +*/ diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs new file mode 100644 index 0000000000..b752584e0b --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia; +using Prism.Extensions; + +namespace Prism.Navigation.Regions +{ + /// + /// Defines a class that wraps an item and adds metadata for it. + /// + public class ItemMetadata : AvaloniaObject + { + /// The name of the wrapped item. + public static readonly StyledProperty NameProperty = AvaloniaProperty.Register(nameof(Name)); + + /// Value indicating whether the wrapped item is considered active. + public static readonly StyledProperty IsActiveProperty = AvaloniaProperty.Register(nameof(IsActive)); + + /// Initializes a new instance of . + /// The item to wrap. + public ItemMetadata(object item) + { + // check for null + Item = item; + } + + static ItemMetadata() + { + IsActiveProperty.Changed.Subscribe(args => StyledPropertyChanged(args?.Sender, args)); + } + + /// Gets the wrapped item. + /// The wrapped item. + public object Item { get; private set; } + + /// Gets or sets a name for the wrapped item. + /// The name of the wrapped item. + public string Name + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + + /// Gets or sets a value indicating whether the wrapped item is considered active. + /// if the item should be considered active; otherwise . + public bool IsActive + { + get => (bool)GetValue(IsActiveProperty); + set => SetValue(IsActiveProperty, value); + } + + /// Occurs when metadata on the item changes. + public event EventHandler MetadataChanged; + + /// Explicitly invokes to notify listeners. + public void InvokeMetadataChanged() + { + MetadataChanged?.Invoke(this, EventArgs.Empty); + } + + private static void StyledPropertyChanged(AvaloniaObject avaloniaObject, AvaloniaPropertyChangedEventArgs args) + { + var itemMetadata = avaloniaObject as ItemMetadata; + itemMetadata?.InvokeMetadataChanged(); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj b/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj new file mode 100644 index 0000000000..fc4d63b7b0 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj @@ -0,0 +1,77 @@ + + + + Properties + Prism + net6.0;net7.0;net8.0 + Prism.Avalonia is a fully open source version of the Prism guidance originally produced by Microsoft Patterns & Practices. Prism.Avalonia provides an implementation of a collection of design patterns that are helpful in writing well structured, maintainable, and testable XAML applications, including MVVM, dependency injection, commanding, event aggregation, and more. Prism's core functionality is a shared library targeting the .NET Framework and .NET Standard. Features that need to be platform specific are implemented in the respective libraries for the target platform (Avalonia, WPF, Uno Platform, and Xamarin Forms). + +Prism.Avalonia helps you more easily design and build rich, flexible, and easy to maintain cross-platform Avalonia desktop applications. This library provides user interface composition as well as modularity support. + + Prism.Avalonia + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + diff --git a/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs b/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs new file mode 100644 index 0000000000..76550acd24 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs @@ -0,0 +1,171 @@ +using Prism.Common; +using Prism.Modularity; +using Prism.Navigation.Regions; + +namespace Prism +{ + /// + /// Base application class that provides a basic initialization sequence + /// + /// + /// This class must be overridden to provide application specific configuration. + /// + public abstract class PrismApplicationBase : Application + { + private IContainerExtension _containerExtension; + private IModuleCatalog _moduleCatalog; + + /// Main window. + public AvaloniaObject MainWindow { get; private set; } + + /// The dependency injection container used to resolve objects. + public IContainerProvider Container => _containerExtension; + + /// Configures the used by Prism. + protected virtual void ConfigureViewModelLocator() + { + PrismInitializationExtensions.ConfigureViewModelLocator(); + } + + /// + /// Runs the initialization sequence to configure the Prism application. + /// + /// + /// Though, Prism.WPF v8.1 uses, `protected virtual void Initialize()` + /// Avalonia's AppBuilderBase.cs calls, `.Setup() { ... Instance.Initialize(); ... }` + /// Therefore, we need this as a `public override void` in PrismApplicationBase.cs + /// + public override void Initialize() + { + base.Initialize(); + + ConfigureViewModelLocator(); + + ContainerLocator.SetContainerExtension(CreateContainerExtension()); + _containerExtension = ContainerLocator.Current; + _moduleCatalog = CreateModuleCatalog(); + RegisterRequiredTypes(_containerExtension); + RegisterTypes(_containerExtension); + + ConfigureModuleCatalog(_moduleCatalog); + + var regionAdapterMappings = _containerExtension.Resolve(); + ConfigureRegionAdapterMappings(regionAdapterMappings); + + var defaultRegionBehaviors = _containerExtension.Resolve(); + ConfigureDefaultRegionBehaviors(defaultRegionBehaviors); + + RegisterFrameworkExceptionTypes(); + + var shell = CreateShell(); + if (shell != null) + { + MvvmHelpers.AutowireViewModel(shell); + RegionManager.SetRegionManager(shell, _containerExtension.Resolve()); + RegionManager.UpdateRegions(); + InitializeShell(shell); + } + + InitializeModules(); + + OnInitialized(); + } + + /// Framework initialization has completed. + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + desktopLifetime.MainWindow = MainWindow as Window; + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + singleViewLifetime.MainView = MainWindow as Control; + + base.OnFrameworkInitializationCompleted(); + } + + /// Creates the container used by Prism. + /// The container + protected abstract IContainerExtension CreateContainerExtension(); + + /// Creates the used by Prism. + /// The base implementation returns a new ModuleCatalog. + protected virtual IModuleCatalog CreateModuleCatalog() + { + return new ModuleCatalog(); + } + + /// Registers all types that are required by Prism to function with the container. + /// + protected virtual void RegisterRequiredTypes(IContainerRegistry containerRegistry) + { + containerRegistry.RegisterRequiredTypes(_moduleCatalog); + } + + /// + /// Used to register types with the container that will be used by your application. + /// + protected abstract void RegisterTypes(IContainerRegistry containerRegistry); + + /// + /// Configures the . + /// This will be the list of default behaviors that will be added to a region. + /// + protected virtual void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) + { + regionBehaviors?.RegisterDefaultRegionBehaviors(); + } + + /// + /// Configures the default region adapter mappings to use in the application, in order + /// to adapt UI controls defined in XAML to use a region and register it automatically. + /// May be overwritten in a derived class to add specific mappings required by the application. + /// + /// The instance containing all the mappings. + protected virtual void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) + { + regionAdapterMappings?.RegisterDefaultRegionAdapterMappings(); + } + + /// + /// Registers the s of the Exceptions that are not considered + /// root exceptions by the . + /// + protected virtual void RegisterFrameworkExceptionTypes() + { + } + + /// + /// Creates the shell or main window of the application. + /// + /// The shell of the application. + protected abstract AvaloniaObject CreateShell(); + + /// + /// Initializes the shell. + /// + protected virtual void InitializeShell(AvaloniaObject shell) + { + MainWindow = shell; + } + + /// + /// Contains actions that should occur last. + /// + protected virtual void OnInitialized() + { + (MainWindow as Window)?.Show(); + } + + /// + /// Configures the used by Prism. + /// + protected virtual void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { } + + /// + /// Initializes the modules. + /// + protected virtual void InitializeModules() + { + PrismInitializationExtensions.RunModuleManager(Container); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs b/src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs new file mode 100644 index 0000000000..cc5a657779 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs @@ -0,0 +1,183 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Prism.Common; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Navigation.Regions; + +namespace Prism +{ + /// + /// Base class that provides a basic bootstrapping sequence and hooks + /// that specific implementations can override + /// + /// + /// This class must be overridden to provide application specific configuration. + /// + public abstract class PrismBootstrapperBase + { + private IContainerExtension _containerExtension; + private IModuleCatalog _moduleCatalog; + + /// + /// The dependency injection container used to resolve objects + /// + public IContainerProvider Container => _containerExtension; + + /// + /// Gets the shell user interface + /// + /// The shell user interface. + protected AvaloniaObject Shell { get; set; } + + /// + /// Runs the bootstrapper process. + /// + public void Run() + { + ConfigureViewModelLocator(); + Initialize(); + OnInitialized(); + } + + /// + /// Configures the used by Prism. + /// + protected virtual void ConfigureViewModelLocator() + { + PrismInitializationExtensions.ConfigureViewModelLocator(); + } + + /// + /// Runs the initialization sequence to configure the Prism application. + /// + protected virtual void Initialize() + { + ContainerLocator.SetContainerExtension(CreateContainerExtension()); + _containerExtension = ContainerLocator.Current; + _moduleCatalog = CreateModuleCatalog(); + RegisterRequiredTypes(_containerExtension); + RegisterTypes(_containerExtension); + + ConfigureModuleCatalog(_moduleCatalog); + + var regionAdapterMappings = _containerExtension.Resolve(); + ConfigureRegionAdapterMappings(regionAdapterMappings); + + var defaultRegionBehaviors = _containerExtension.Resolve(); + ConfigureDefaultRegionBehaviors(defaultRegionBehaviors); + + RegisterFrameworkExceptionTypes(); + + var shell = CreateShell(); + if (shell != null) + { + MvvmHelpers.AutowireViewModel(shell); + RegionManager.SetRegionManager(shell, _containerExtension.Resolve()); + RegionManager.UpdateRegions(); + InitializeShell(shell); + } + + InitializeModules(); + } + + /// + /// Creates the container used by Prism. + /// + /// The container + protected abstract IContainerExtension CreateContainerExtension(); + + /// + /// Creates the used by Prism. + /// + /// + /// The base implementation returns a new ModuleCatalog. + /// + protected virtual IModuleCatalog CreateModuleCatalog() + { + return new ModuleCatalog(); + } + + /// + /// Registers all types that are required by Prism to function with the container. + /// + /// + protected virtual void RegisterRequiredTypes(IContainerRegistry containerRegistry) + { + if (_moduleCatalog == null) + throw new InvalidOperationException("IModuleCatalog was null"); + + containerRegistry.RegisterRequiredTypes(_moduleCatalog); + } + + /// + /// Used to register types with the container that will be used by your application. + /// + protected abstract void RegisterTypes(IContainerRegistry containerRegistry); + + /// + /// Configures the . + /// This will be the list of default behaviors that will be added to a region. + /// + protected virtual void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) + { + regionBehaviors?.RegisterDefaultRegionBehaviors(); + } + + /// + /// Configures the default region adapter mappings to use in the application, in order + /// to adapt UI controls defined in XAML to use a region and register it automatically. + /// May be overwritten in a derived class to add specific mappings required by the application. + /// + /// The instance containing all the mappings. + protected virtual void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) + { + regionAdapterMappings?.RegisterDefaultRegionAdapterMappings(); + } + + /// + /// Registers the s of the Exceptions that are not considered + /// root exceptions by the . + /// + protected virtual void RegisterFrameworkExceptionTypes() + { + } + + /// + /// Creates the shell or main window of the application. + /// + /// The shell of the application. + protected abstract AvaloniaObject CreateShell(); + + /// + /// Initializes the shell. + /// + protected virtual void InitializeShell(AvaloniaObject shell) + { + Shell = shell; + } + + /// + /// Contains actions that should occur last. + /// + protected virtual void OnInitialized() + { + if (Shell is Window window) + window.Show(); + } + + /// + /// Configures the used by Prism. + /// + protected virtual void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { } + + /// + /// Initializes the modules. + /// + protected virtual void InitializeModules() + { + PrismInitializationExtensions.RunModuleManager(Container); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs b/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..09e2fa849c --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Metadata; + +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] + +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Navigation.Regions")] +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Navigation.Regions.Behaviors")] +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Mvvm")] +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Interactivity")] +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Dialogs")] +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Ioc")] + +[assembly: InternalsVisibleTo("Prism.Avalonia.Tests")] diff --git a/src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs b/src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..234cbd6d72 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs @@ -0,0 +1,523 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Prism.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Prism.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The object must be of type '{0}' in order to use the current region adapter.. + /// + internal static string AdapterInvalidTypeException { + get { + return ResourceManager.GetString("AdapterInvalidTypeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot change the region name once is set. The current region name is '{0}'.. + /// + internal static string CannotChangeRegionNameException { + get { + return ResourceManager.GetString("CannotChangeRegionNameException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot create navigation target '{0}'.. + /// + internal static string CannotCreateNavigationTarget { + get { + return ResourceManager.GetString("CannotCreateNavigationTarget", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' does not implement from IRegionBehavior.. + /// + internal static string CanOnlyAddTypesThatInheritIFromRegionBehavior { + get { + return ResourceManager.GetString("CanOnlyAddTypesThatInheritIFromRegionBehavior", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ConfigurationStore cannot contain a null value. . + /// + internal static string ConfigurationStoreCannotBeNull { + get { + return ResourceManager.GetString("ConfigurationStoreCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ContentControl's Content property is not empty. + /// This control is being associated with a region, but the control is already bound to something else. + /// If you did not explicitly set the control's Content property, + /// this exception may be caused by a change in the value of the inherited RegionManager attached property.. + /// + internal static string ContentControlHasContentException { + get { + return ResourceManager.GetString("ContentControlHasContentException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deactivation is not possible in this type of region.. + /// + internal static string DeactiveNotPossibleException { + get { + return ResourceManager.GetString("DeactiveNotPossibleException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {1}: {2}. Priority: {3}. Timestamp:{0:u}.. + /// + internal static string DefaultTextLoggerPattern { + get { + return ResourceManager.GetString("DefaultTextLoggerPattern", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Neither the executeMethod nor the canExecuteMethod delegates can be null.. + /// + internal static string DelegateCommandDelegatesCannotBeNull { + get { + return ResourceManager.GetString("DelegateCommandDelegatesCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to T for DelegateCommand<T> is not an object nor Nullable.. + /// + internal static string DelegateCommandInvalidGenericPayloadType { + get { + return ResourceManager.GetString("DelegateCommandInvalidGenericPayloadType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Directory {0} was not found.. + /// + internal static string DirectoryNotFound { + get { + return ResourceManager.GetString("DirectoryNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A duplicated module group with name {0} has been found by the loader.. + /// + internal static string DuplicatedModuleGroup { + get { + return ResourceManager.GetString("DuplicatedModuleGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to retrieve the module type {0} from the loaded assemblies. You may need to specify a more fully-qualified type name.. + /// + internal static string FailedToGetType { + get { + return ResourceManager.GetString("FailedToGetType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HostControl cannot have null value when behavior attaches. . + /// + internal static string HostControlCannotBeNull { + get { + return ResourceManager.GetString("HostControlCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The HostControl property cannot be set after Attach method has been called.. + /// + internal static string HostControlCannotBeSetAfterAttach { + get { + return ResourceManager.GetString("HostControlCannotBeSetAfterAttach", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HostControl type must be a TabControl.. + /// + internal static string HostControlMustBeATabControl { + get { + return ResourceManager.GetString("HostControlMustBeATabControl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The IModuleEnumerator interface is no longer used and has been replaced by ModuleCatalog.. + /// + internal static string IEnumeratorObsolete { + get { + return ResourceManager.GetString("IEnumeratorObsolete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument must be a valid absolute Uri to an assembly file.. + /// + internal static string InvalidArgumentAssemblyUri { + get { + return ResourceManager.GetString("InvalidArgumentAssemblyUri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Target of the IDelegateReference should be of type {0}.. + /// + internal static string InvalidDelegateRerefenceTypeException { + get { + return ResourceManager.GetString("InvalidDelegateRerefenceTypeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ItemsControl's ItemsSource property is not empty. + /// This control is being associated with a region, but the control is already bound to something else. + /// If you did not explicitly set the control's ItemSource property, + /// this exception may be caused by a change in the value of the inherited RegionManager attached property.. + /// + internal static string ItemsControlHasItemsSourceException { + get { + return ResourceManager.GetString("ItemsControlHasItemsSourceException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mapping with the given type is already registered: {0}.. + /// + internal static string MappingExistsException { + get { + return ResourceManager.GetString("MappingExistsException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Module {0} was not found in the catalog.. + /// + internal static string ModuleNotFound { + get { + return ResourceManager.GetString("ModuleNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ModulePath cannot contain a null value or be empty. + /// + internal static string ModulePathCannotBeNullOrEmpty { + get { + return ResourceManager.GetString("ModulePathCannotBeNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to load type '{0}' from assembly '{1}'.. + /// + internal static string ModuleTypeNotFound { + get { + return ResourceManager.GetString("ModuleTypeNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ModuleCatalog must implement IModuleGroupCatalog to add groups. + /// + internal static string MustBeModuleGroupCatalog { + get { + return ResourceManager.GetString("MustBeModuleGroupCatalog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Navigation is already in progress on region with name '{0}'.. + /// + internal static string NavigationInProgress { + get { + return ResourceManager.GetString("NavigationInProgress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Navigation cannot proceed until a region is set for the RegionNavigationService.. + /// + internal static string NavigationServiceHasNoRegion { + get { + return ResourceManager.GetString("NavigationServiceHasNoRegion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The IRegionAdapter for the type {0} is not registered in the region adapter mappings. You can register an IRegionAdapter for this control by overriding the ConfigureRegionAdapterMappings method in the bootstrapper.. + /// + internal static string NoRegionAdapterException { + get { + return ResourceManager.GetString("NoRegionAdapterException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is currently no moduleTypeLoader in the ModuleManager that can retrieve the specified module.. + /// + internal static string NoRetrieverCanRetrieveModule { + get { + return ResourceManager.GetString("NoRetrieverCanRetrieveModule", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An exception has occurred while trying to add a view to region '{0}'. + /// - The most likely causing exception was was: '{1}'. + /// But also check the InnerExceptions for more detail or call .GetRootException(). . + /// + internal static string OnViewRegisteredException { + get { + return ResourceManager.GetString("OnViewRegisteredException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The member access expression does not access a property.. + /// + internal static string PropertySupport_ExpressionNotProperty_Exception { + get { + return ResourceManager.GetString("PropertySupport_ExpressionNotProperty_Exception", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The expression is not a member access expression.. + /// + internal static string PropertySupport_NotMemberAccessExpression_Exception { + get { + return ResourceManager.GetString("PropertySupport_NotMemberAccessExpression_Exception", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The referenced property is a static property.. + /// + internal static string PropertySupport_StaticExpression_Exception { + get { + return ResourceManager.GetString("PropertySupport_StaticExpression_Exception", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Attach method cannot be called when Region property is null.. + /// + internal static string RegionBehaviorAttachCannotBeCallWithNullRegion { + get { + return ResourceManager.GetString("RegionBehaviorAttachCannotBeCallWithNullRegion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Region property cannot be set after Attach method has been called.. + /// + internal static string RegionBehaviorRegionCannotBeSetAfterAttach { + get { + return ResourceManager.GetString("RegionBehaviorRegionCannotBeSetAfterAttach", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An exception occurred while creating a region with name '{0}'. The exception was: {1}. . + /// + internal static string RegionCreationException { + get { + return ResourceManager.GetString("RegionCreationException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The region being added already has a name of '{0}' and cannot be added to the region manager with a different name ('{1}').. + /// + internal static string RegionManagerWithDifferentNameException { + get { + return ResourceManager.GetString("RegionManagerWithDifferentNameException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The region name cannot be null or empty.. + /// + internal static string RegionNameCannotBeEmptyException { + get { + return ResourceManager.GetString("RegionNameCannotBeEmptyException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Region with the given name is already registered: {0}. + /// + internal static string RegionNameExistsException { + get { + return ResourceManager.GetString("RegionNameExistsException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This RegionManager does not contain a Region with the name '{0}'.. + /// + internal static string RegionNotFound { + get { + return ResourceManager.GetString("RegionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The region manager does not contain the {0} region.. + /// + internal static string RegionNotInRegionManagerException { + get { + return ResourceManager.GetString("RegionNotInRegionManagerException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View already exists in region.. + /// + internal static string RegionViewExistsException { + get { + return ResourceManager.GetString("RegionViewExistsException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View with name '{0}' already exists in the region.. + /// + internal static string RegionViewNameExistsException { + get { + return ResourceManager.GetString("RegionViewNameExistsException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The provided String argument {0} must not be null or empty.. + /// + internal static string StringCannotBeNullOrEmpty { + get { + return ResourceManager.GetString("StringCannotBeNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The provided String argument {0} must not be null or empty.. + /// + internal static string StringCannotBeNullOrEmpty1 { + get { + return ResourceManager.GetString("StringCannotBeNullOrEmpty1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No BehaviorType with key '{0}' was registered.. + /// + internal static string TypeWithKeyNotRegistered { + get { + return ResourceManager.GetString("TypeWithKeyNotRegistered", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An exception occurred while trying to create region objects. + /// - The most likely causing exception was: '{0}'. + /// But also check the InnerExceptions for more detail or call .GetRootException(). . + /// + internal static string UpdateRegionException { + get { + return ResourceManager.GetString("UpdateRegionException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value must be of type ModuleInfo.. + /// + internal static string ValueMustBeOfTypeModuleInfo { + get { + return ResourceManager.GetString("ValueMustBeOfTypeModuleInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} not found.. + /// + internal static string ValueNotFound { + get { + return ResourceManager.GetString("ValueNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The region does not contain the specified view.. + /// + internal static string ViewNotInRegionException { + get { + return ResourceManager.GetString("ViewNotInRegionException", resourceCulture); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Properties/Resources.resx b/src/Avalonia/Prism.Avalonia/Properties/Resources.resx new file mode 100644 index 0000000000..3e88851bcf --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Properties/Resources.resx @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The object must be of type '{0}' in order to use the current region adapter. + + + Cannot change the region name once is set. The current region name is '{0}'. + + + Cannot create navigation target '{0}'. + + + Type '{0}' does not implement from IRegionBehavior. + + + The ConfigurationStore cannot contain a null value. + + + ContentControl's Content property is not empty. + This control is being associated with a region, but the control is already bound to something else. + If you did not explicitly set the control's Content property, + this exception may be caused by a change in the value of the inherited RegionManager attached property. + + + Deactivation is not possible in this type of region. + + + {1}: {2}. Priority: {3}. Timestamp:{0:u}. + + + Neither the executeMethod nor the canExecuteMethod delegates can be null. + + + T for DelegateCommand<T> is not an object nor Nullable. + + + Directory {0} was not found. + + + A duplicated module group with name {0} has been found by the loader. + + + Unable to retrieve the module type {0} from the loaded assemblies. You may need to specify a more fully-qualified type name. + + + HostControl cannot have null value when behavior attaches. + + + The HostControl property cannot be set after Attach method has been called. + + + HostControl type must be a TabControl. + + + The IModuleEnumerator interface is no longer used and has been replaced by ModuleCatalog. + + + The argument must be a valid absolute Uri to an assembly file. + + + The Target of the IDelegateReference should be of type {0}. + + + ItemsControl's ItemsSource property is not empty. + This control is being associated with a region, but the control is already bound to something else. + If you did not explicitly set the control's ItemSource property, + this exception may be caused by a change in the value of the inherited RegionManager attached property. + + + Mapping with the given type is already registered: {0}. + + + Module {0} was not found in the catalog. + + + The ModulePath cannot contain a null value or be empty + + + Failed to load type '{0}' from assembly '{1}'. + + + Navigation is already in progress on region with name '{0}'. + + + Navigation cannot proceed until a region is set for the RegionNavigationService. + + + The IRegionAdapter for the type {0} is not registered in the region adapter mappings. You can register an IRegionAdapter for this control by overriding the ConfigureRegionAdapterMappings method in the bootstrapper. + + + There is currently no moduleTypeLoader in the ModuleManager that can retrieve the specified module. + + + An exception has occurred while trying to add a view to region '{0}'. + - The most likely causing exception was was: '{1}'. + But also check the InnerExceptions for more detail or call .GetRootException(). + + + The member access expression does not access a property. + + + The expression is not a member access expression. + + + The referenced property is a static property. + + + The Attach method cannot be called when Region property is null. + + + The Region property cannot be set after Attach method has been called. + + + An exception occurred while creating a region with name '{0}'. The exception was: {1}. + + + The region being added already has a name of '{0}' and cannot be added to the region manager with a different name ('{1}'). + + + The region name cannot be null or empty. + + + Region with the given name is already registered: {0} + + + This RegionManager does not contain a Region with the name '{0}'. + + + The region manager does not contain the {0} region. + + + View already exists in region. + + + View with name '{0}' already exists in the region. + + + The provided String argument {0} must not be null or empty. + + + The provided String argument {0} must not be null or empty. + + + No BehaviorType with key '{0}' was registered. + + + An exception occurred while trying to create region objects. + - The most likely causing exception was: '{0}'. + But also check the InnerExceptions for more detail or call .GetRootException(). + + + The value must be of type ModuleInfo. + + + {0} not found. + + + The region does not contain the specified view. + + + The ModuleCatalog must implement IModuleGroupCatalog to add groups + + \ No newline at end of file diff --git a/src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs b/src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs new file mode 100644 index 0000000000..7dbe76a392 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Prism.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Properties/Settings.settings b/src/Avalonia/Prism.Avalonia/Properties/Settings.settings new file mode 100644 index 0000000000..9010101f83 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj b/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj new file mode 100644 index 0000000000..2f6b60c4a7 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj @@ -0,0 +1,56 @@ + + + + Prism.DryIoc + net6.0;net7.0;net8.0 + This extension is used to build Prism.Avalonia applications based on DryIoc. Users must install the Prism.Avalonia NuGet package as well. + Damian Suess, Suess Labs, various contributors + Copyright (c) 2024 Xeno Innovations, Inc. + Prism.DryIoc.Avalonia + README.md + prism;mvvm;xaml;avalonia;dryioc;dependencyinjection;navigation;dialog;prismavalonia; + Prism.Avalonia.png + + + + + True + \ + + + True + \ + + + + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b6488c1811 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Metadata; + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] +[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.DryIoc")] +[assembly: InternalsVisibleTo("Prism.DryIoc.Avalonia.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001008f34b619d7a39e44cebe5ccbd5607eaa0784c9c124431ba336a14e4fecd874f151b57163961505e76943910c7cabea9c7229edc3553dfc33ac7b269087e5cef9404bdb491907ffd9f9b26d737fa2c359620a2cbf2802f54118471d7c0ead3b95c916783dd4b99b9b1a0cd2785e1b5d614d3d9140a60c8c64c217e1c2b0ec8296")] diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..55e46dea13 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs @@ -0,0 +1,270 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Prism.DryIoc.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Prism.DryIoc.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to a. + /// + public static string a { + get { + return ResourceManager.GetString("a", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bootstrapper sequence completed.. + /// + public static string BootstrapperSequenceCompleted { + get { + return ResourceManager.GetString("BootstrapperSequenceCompleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuring default region behaviors.. + /// + public static string ConfiguringDefaultRegionBehaviors { + get { + return ResourceManager.GetString("ConfiguringDefaultRegionBehaviors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuring the DryIoc container.. + /// + public static string ConfiguringDryIocContainer { + get { + return ResourceManager.GetString("ConfiguringDryIocContainer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuring module catalog.. + /// + public static string ConfiguringModuleCatalog { + get { + return ResourceManager.GetString("ConfiguringModuleCatalog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuring region adapters.. + /// + public static string ConfiguringRegionAdapters { + get { + return ResourceManager.GetString("ConfiguringRegionAdapters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuring ServiceLocator singleton.. + /// + public static string ConfiguringServiceLocatorSingleton { + get { + return ResourceManager.GetString("ConfiguringServiceLocatorSingleton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuring the ViewModelLocator to use DryIoc.. + /// + public static string ConfiguringViewModelLocator { + get { + return ResourceManager.GetString("ConfiguringViewModelLocator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creating DryIoc container.. + /// + public static string CreatingDryIocContainer { + get { + return ResourceManager.GetString("CreatingDryIocContainer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creating module catalog.. + /// + public static string CreatingModuleCatalog { + get { + return ResourceManager.GetString("CreatingModuleCatalog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creating the shell.. + /// + public static string CreatingShell { + get { + return ResourceManager.GetString("CreatingShell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Initializing modules.. + /// + public static string InitializingModules { + get { + return ResourceManager.GetString("InitializingModules", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Initializing the shell.. + /// + public static string InitializingShell { + get { + return ResourceManager.GetString("InitializingShell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logger was created successfully.. + /// + public static string LoggerCreatedSuccessfully { + get { + return ResourceManager.GetString("LoggerCreatedSuccessfully", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The method 'GetModuleEnumerator' of the bootstrapper must be overwritten in order to use the default module initialization logic.. + /// + public static string NotOverwrittenGetModuleEnumeratorException { + get { + return ResourceManager.GetString("NotOverwrittenGetModuleEnumeratorException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ContainerBuilder is required and cannot be null.. + /// + public static string NullDryIocContainerBuilderException { + get { + return ResourceManager.GetString("NullDryIocContainerBuilderException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The IContainer is required and cannot be null.. + /// + public static string NullDryIocContainerException { + get { + return ResourceManager.GetString("NullDryIocContainerException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ILoggerFacade is required and cannot be null.. + /// + public static string NullLoggerFacadeException { + get { + return ResourceManager.GetString("NullLoggerFacadeException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The IModuleCatalog is required and cannot be null in order to initialize the modules.. + /// + public static string NullModuleCatalogException { + get { + return ResourceManager.GetString("NullModuleCatalogException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Registering Framework Exception Types.. + /// + public static string RegisteringFrameworkExceptionTypes { + get { + return ResourceManager.GetString("RegisteringFrameworkExceptionTypes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Setting the RegionManager.. + /// + public static string SettingTheRegionManager { + get { + return ResourceManager.GetString("SettingTheRegionManager", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' was already registered by the application. Skipping.... + /// + public static string TypeMappingAlreadyRegistered { + get { + return ResourceManager.GetString("TypeMappingAlreadyRegistered", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updating Regions.. + /// + public static string UpdatingRegions { + get { + return ResourceManager.GetString("UpdatingRegions", resourceCulture); + } + } + } +} diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx new file mode 100644 index 0000000000..e69f6db742 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + a + + + Bootstrapper sequence completed. + + + Configuring default region behaviors. + + + Configuring the DryIoc container. + + + Configuring module catalog. + + + Configuring region adapters. + + + Configuring ServiceLocator singleton. + + + Configuring the ViewModelLocator to use DryIoc. + + + Creating DryIoc container. + + + Creating module catalog. + + + Creating the shell. + + + Initializing modules. + + + Initializing the shell. + + + Logger was created successfully. + + + The method 'GetModuleEnumerator' of the bootstrapper must be overwritten in order to use the default module initialization logic. + + + The ContainerBuilder is required and cannot be null. + + + The IContainer is required and cannot be null. + + + The ILoggerFacade is required and cannot be null. + + + The IModuleCatalog is required and cannot be null in order to initialize the modules. + + + Registering Framework Exception Types. + + + Setting the RegionManager. + + + Type '{0}' was already registered by the application. Skipping... + + + Updating Regions. + + \ No newline at end of file diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets b/src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets new file mode 100644 index 0000000000..73949a907a --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Avalonia/ReadMe.md b/src/Avalonia/ReadMe.md new file mode 100644 index 0000000000..bf72fc7ad9 --- /dev/null +++ b/src/Avalonia/ReadMe.md @@ -0,0 +1,16 @@ + +Thank you for installing Prism.Avalonia v9.0! + +## Help Support Prism + +While Prism.Avalonia is, and will continue to be totally free to download, Open Source is not free. If you or your company have the resources, please consider becoming a GitHub Sponsor. GitHub Sponsorships help to make Open Source Development more sustainable. + +## Support & FAQ + +1) Support: Prism is distributed under the MIT License this means "AS IS", WITHOUT WARRANTY OF ANY KIND. If you require code level support, architectural guidance, or custom builds of Prism, please reach out at https://github.com/PrismLibrary/Prism to inquire about Enterprise support options. + +2) Community Support: GitHub issues are not the place to ask questions, these are reserved for feature requests and legitimate bug reports. You are free to ask questions on Stack Overflow however the Prism team does not monitor questions posted there. We do encourage you to post questions using GitHub Discussions on the main Prism repo. If you see a question that you know the answer to please pay it forward and help other developers that may just be starting out. https://github.com/PrismLibrary/Prism/discussions + +3) Reporting Bugs: If you believe you have encountered a bug, please be sure to search the open and closed issues as you may find the issue has already been fixed as and is awaiting release. If you find that you have a new issue, please create a new project that focuses on ONLY the necessary steps to reproduce the issue you are facing. Issues that are opened which do not have a sample app reproducing the issue will be closed. In addition to this being required by the Prism team, this is just good etiquette for any Open Source project. Additionally we ask that you do not try to be an Archaeologist, trying to comment on PR's, commits and issues from long ago, if there is a legitimate issue, open a new issue referencing what you need to reference. Archaeologists will be ignored. + +4) Samples: Currently the Prism.Avalonia team maintains a wide variety of samples for Avalonia. If you would like to help give back, and help build out a sample(s) repo please contact the Prism.Avalonia team via the GitHub Discussions forum. diff --git a/src/Wpf/Prism.DryIoc.Wpf/GlobalSuppressions.cs b/src/Wpf/Prism.DryIoc.Wpf/GlobalSuppressions.cs index 9284537b0f..a69d206a20 100644 --- a/src/Wpf/Prism.DryIoc.Wpf/GlobalSuppressions.cs +++ b/src/Wpf/Prism.DryIoc.Wpf/GlobalSuppressions.cs @@ -1,5 +1,3 @@ - - // This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given diff --git a/src/Wpf/Prism.Wpf/Interactivity/CommandBehaviorBase.cs b/src/Wpf/Prism.Wpf/Interactivity/CommandBehaviorBase.cs index bfcb8b58ed..aa9aa9a901 100644 --- a/src/Wpf/Prism.Wpf/Interactivity/CommandBehaviorBase.cs +++ b/src/Wpf/Prism.Wpf/Interactivity/CommandBehaviorBase.cs @@ -1,4 +1,7 @@ using System.Windows.Input; +#if AVALONIA +using Avalonia.Controls; +#endif namespace Prism.Interactivity { @@ -9,7 +12,11 @@ namespace Prism.Interactivity /// /// CommandBehaviorBase can be used to provide new behaviors for commands. /// +#if !AVALONIA public class CommandBehaviorBase where T : UIElement +#else + public class CommandBehaviorBase where T : Control +#endif { private ICommand _command; private object _commandParameter; @@ -91,7 +98,6 @@ protected T TargetObject } } - /// /// Updates the target object's IsEnabled property based on the commands ability to execute. /// diff --git a/src/Wpf/Prism.Wpf/Modularity/DirectoryModuleCatalog.netcore.cs b/src/Wpf/Prism.Wpf/Modularity/DirectoryModuleCatalog.netcore.cs index 6d81be2a72..31e9bb2665 100644 --- a/src/Wpf/Prism.Wpf/Modularity/DirectoryModuleCatalog.netcore.cs +++ b/src/Wpf/Prism.Wpf/Modularity/DirectoryModuleCatalog.netcore.cs @@ -46,12 +46,12 @@ protected override void InnerLoad() List loadedAssemblies = new List(); var assemblies = ( - from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies() - where !(assembly is System.Reflection.Emit.AssemblyBuilder) - && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder" - && !String.IsNullOrEmpty(assembly.Location) - select assembly.Location - ); + from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies() + where !(assembly is System.Reflection.Emit.AssemblyBuilder) + && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder" + && !String.IsNullOrEmpty(assembly.Location) + select assembly.Location + ); loadedAssemblies.AddRange(assemblies); @@ -132,12 +132,14 @@ private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, Directory { return loadedAssembly; } + AssemblyName assemblyName = new AssemblyName(args.Name); string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll"); if (File.Exists(dependentAssemblyFilename)) { return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename); } + return Assembly.ReflectionOnlyLoad(args.Name); } @@ -202,6 +204,7 @@ private static ModuleInfo CreateModuleInfo(Type type) InitializationMode = onDemand ? InitializationMode.OnDemand : InitializationMode.WhenAvailable, Ref = type.Assembly.EscapedCodeBase, }; + moduleInfo.DependsOn.AddRange(dependsOn); return moduleInfo; } diff --git a/src/Wpf/Prism.Wpf/Modularity/FileModuleTypeLoader.Desktop.cs b/src/Wpf/Prism.Wpf/Modularity/FileModuleTypeLoader.Desktop.cs index 0dade0ff1e..e8c8cc1321 100644 --- a/src/Wpf/Prism.Wpf/Modularity/FileModuleTypeLoader.Desktop.cs +++ b/src/Wpf/Prism.Wpf/Modularity/FileModuleTypeLoader.Desktop.cs @@ -82,7 +82,6 @@ public bool CanLoadModuleType(IModuleInfo moduleInfo) return moduleInfo.Ref != null && moduleInfo.Ref.StartsWith(RefFilePrefix, StringComparison.Ordinal); } - /// /// Retrieves the . /// diff --git a/src/Wpf/Prism.Wpf/Modularity/ModuleCatalog.cs b/src/Wpf/Prism.Wpf/Modularity/ModuleCatalog.cs index 0acda50694..caa2c29c5f 100644 --- a/src/Wpf/Prism.Wpf/Modularity/ModuleCatalog.cs +++ b/src/Wpf/Prism.Wpf/Modularity/ModuleCatalog.cs @@ -1,5 +1,11 @@ using System.IO; +#if !AVALONIA using System.Windows.Markup; +#else +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Metadata; +#endif namespace Prism.Modularity { @@ -20,7 +26,10 @@ namespace Prism.Modularity /// /// The also serves as a baseclass for more specialized Catalogs . /// + /// Avalonia does use, System.Windows.Markup. See property, `Items` below. +#if !AVALONIA [ContentProperty("Items")] +#endif public class ModuleCatalog : ModuleCatalogBase, IModuleGroupsCatalog { /// @@ -39,6 +48,16 @@ public ModuleCatalog(IEnumerable modules) : base(modules) { } +#if AVALONIA + /// + /// Gets the items in the Prism.Modularity.IModuleCatalog. This property is mainly + /// used to add Prism.Modularity.IModuleInfoGroups or Prism.Modularity.IModuleInfos + /// through XAML. + /// + [Content] + public new Collection Items => base.Items; +#endif + /// /// Creates a valid file uri to locate the module assembly file /// diff --git a/src/Wpf/Prism.Wpf/Modularity/ModuleInfoGroup.cs b/src/Wpf/Prism.Wpf/Modularity/ModuleInfoGroup.cs index 69b03ae3a0..c5f98c3939 100644 --- a/src/Wpf/Prism.Wpf/Modularity/ModuleInfoGroup.cs +++ b/src/Wpf/Prism.Wpf/Modularity/ModuleInfoGroup.cs @@ -187,7 +187,6 @@ bool IList.Contains(object value) if (value == null) throw new ArgumentNullException(nameof(value)); - if (!(value is IModuleInfo moduleInfo)) throw new ArgumentException(Resources.ValueMustBeOfTypeModuleInfo, nameof(value)); @@ -228,7 +227,6 @@ public void Insert(int index, object value) if (value == null) throw new ArgumentNullException(nameof(value)); - if (!(value is IModuleInfo moduleInfo)) throw new ArgumentException(Resources.ValueMustBeOfTypeModuleInfo, nameof(value)); diff --git a/src/Wpf/Prism.Wpf/Modularity/ModuleManager.Desktop.cs b/src/Wpf/Prism.Wpf/Modularity/ModuleManager.Desktop.cs index f10f560975..ae652004c3 100644 --- a/src/Wpf/Prism.Wpf/Modularity/ModuleManager.Desktop.cs +++ b/src/Wpf/Prism.Wpf/Modularity/ModuleManager.Desktop.cs @@ -1,13 +1,13 @@ namespace Prism.Modularity { /// - /// Component responsible for coordinating the modules' type loading and module initialization process. + /// Component responsible for coordinating the modules' type loading and module initialization process. /// public partial class ModuleManager { /// - /// Returns the list of registered instances that will be - /// used to load the types of modules. + /// Returns the list of registered instances that will be + /// used to load the types of modules. /// /// The module type loaders. public virtual IEnumerable ModuleTypeLoaders @@ -15,6 +15,5 @@ public virtual IEnumerable ModuleTypeLoaders get => _typeLoaders ??= [new FileModuleTypeLoader()]; set => _typeLoaders = value; } - } } diff --git a/src/Wpf/Prism.Wpf/Modularity/ModuleTypeLoaderNotFoundException.cs b/src/Wpf/Prism.Wpf/Modularity/ModuleTypeLoaderNotFoundException.cs index 4ef1e59693..774efb208e 100644 --- a/src/Wpf/Prism.Wpf/Modularity/ModuleTypeLoaderNotFoundException.cs +++ b/src/Wpf/Prism.Wpf/Modularity/ModuleTypeLoaderNotFoundException.cs @@ -1,8 +1,8 @@ namespace Prism.Modularity { /// - /// Exception that's thrown when there is no registered in - /// that can handle this particular type of module. + /// Exception that's thrown when there is no registered in + /// that can handle this particular type of module. /// public partial class ModuleTypeLoaderNotFoundException : ModularityException { @@ -17,7 +17,7 @@ public ModuleTypeLoaderNotFoundException() /// Initializes a new instance of the class with a specified error message. /// /// - /// The message that describes the error. + /// The message that describes the error. /// public ModuleTypeLoaderNotFoundException(string message) : base(message) @@ -28,7 +28,7 @@ public ModuleTypeLoaderNotFoundException(string message) /// Initializes a new instance of the class with a specified error message. /// /// - /// The message that describes the error. + /// The message that describes the error. /// /// The inner exception public ModuleTypeLoaderNotFoundException(string message, Exception innerException) @@ -41,7 +41,7 @@ public ModuleTypeLoaderNotFoundException(string message, Exception innerExceptio /// /// The name of the module. /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, + /// The exception that is the cause of the current exception, /// or a reference if no inner exception is specified. public ModuleTypeLoaderNotFoundException(string moduleName, string message, Exception innerException) : base(moduleName, message, innerException) diff --git a/src/Wpf/Prism.Wpf/Modularity/XamlModuleCatalog.cs b/src/Wpf/Prism.Wpf/Modularity/XamlModuleCatalog.cs index 2c2cdb0a62..c7a153c02e 100644 --- a/src/Wpf/Prism.Wpf/Modularity/XamlModuleCatalog.cs +++ b/src/Wpf/Prism.Wpf/Modularity/XamlModuleCatalog.cs @@ -1,5 +1,7 @@ +#if !AVALONIA using System.IO; using System.Windows.Markup; +//// using Avalonia.Markup.Xaml; namespace Prism.Modularity { @@ -93,7 +95,11 @@ private static ModuleCatalog CreateFromXaml(Stream xamlStream) throw new ArgumentNullException(nameof(xamlStream)); } +#if !AVALONIA return XamlReader.Load(xamlStream) as ModuleCatalog; +#else + return AvaloniaRuntimeXamlLoader.Load(xamlStream, null) as ModuleCatalog; +#endif } /// @@ -114,3 +120,5 @@ private static ModuleCatalog CreateFromXaml(Uri builderResourceUri) } } } +#endif + diff --git a/src/Wpf/Prism.Wpf/Mvvm/ViewModelLocator.cs b/src/Wpf/Prism.Wpf/Mvvm/ViewModelLocator.cs index c43fe139e1..25d06d4f05 100644 --- a/src/Wpf/Prism.Wpf/Mvvm/ViewModelLocator.cs +++ b/src/Wpf/Prism.Wpf/Mvvm/ViewModelLocator.cs @@ -7,10 +7,27 @@ namespace Prism.Mvvm /// public static class ViewModelLocator { +#if AVALONIA + static ViewModelLocator() + { + // Bind AutoWireViewModelProperty.Changed to its callback + AutoWireViewModelProperty.Changed.Subscribe(args => AutoWireViewModelChanged(args?.Sender, args)); + } + + /// + /// The AutoWireViewModel attached property. + /// + public static AvaloniaProperty AutoWireViewModelProperty = + AvaloniaProperty.RegisterAttached( + name: "AutoWireViewModel", + ownerType: typeof(ViewModelLocator), + defaultValue: null); +#else /// /// The AutoWireViewModel attached property. /// public static DependencyProperty AutoWireViewModelProperty = DependencyProperty.RegisterAttached("AutoWireViewModel", typeof(bool?), typeof(ViewModelLocator), new PropertyMetadata(defaultValue: null, propertyChangedCallback: AutoWireViewModelChanged)); +#endif /// /// Gets the value for the attached property. @@ -34,7 +51,11 @@ public static void SetAutoWireViewModel(DependencyObject obj, bool? value) private static void AutoWireViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { +#if AVALONIA + if (!Design.IsDesignMode) +#else if (!DesignerProperties.GetIsInDesignMode(d)) +#endif { var value = (bool?)e.NewValue; if (value.HasValue && value.Value) diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs index ad5b106ea4..33bcef9992 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs @@ -18,8 +18,13 @@ public class ClearChildViewsRegionBehavior : RegionBehavior /// /// This attached property can be defined on a view to indicate that regions defined in it must be removed from the region manager when the parent view gets removed from a region. /// +#if !AVALONIA public static readonly DependencyProperty ClearChildViewsProperty = DependencyProperty.RegisterAttached("ClearChildViews", typeof(bool), typeof(ClearChildViewsRegionBehavior), new PropertyMetadata(false)); +#else + public static readonly AvaloniaProperty ClearChildViewsProperty = + AvaloniaProperty.RegisterAttached("ClearChildViews", typeof(ClearChildViewsRegionBehavior)); +#endif /// /// Gets the ClearChildViews attached property from a DependencyObject. @@ -48,7 +53,7 @@ public static void SetClearChildViews(DependencyObject target, bool value) } /// - /// Subscribes to the 's PropertyChanged method to monitor its RegionManager property. + /// Subscribes to the 's PropertyChanged method to monitor its property. /// protected override void OnAttach() { diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs index 4b8507620b..535ef7c9ce 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs @@ -1,6 +1,9 @@ using System.Collections.ObjectModel; using System.Globalization; using Prism.Properties; +#if AVALONIA +using Avalonia.Threading; +#endif namespace Prism.Navigation.Regions.Behaviors { @@ -94,7 +97,11 @@ private void TryCreateRegion() return; } +#if !AVALONIA if (targetElement.CheckAccess()) +#else + if (Dispatcher.UIThread.CheckAccess()) +#endif { Detach(); @@ -132,7 +139,11 @@ protected virtual IRegion CreateRegion(DependencyObject targetElement, string re } } +#if !AVALONIA private void ElementLoaded(object sender, RoutedEventArgs e) +#else + private void ElementLoaded(object sender, VisualTreeAttachmentEventArgs e) +#endif { UnWireTargetElement(); TryCreateRegion(); @@ -143,11 +154,15 @@ private void WireUpTargetElement() FrameworkElement element = TargetElement as FrameworkElement; if (element != null) { +#if !AVALONIA element.Loaded += ElementLoaded; +#else + element.AttachedToVisualTree += ElementLoaded; +#endif return; } -#if !UNO_WINUI +#if (!UNO_WINUI && !AVALONIA) FrameworkContentElement fcElement = TargetElement as FrameworkContentElement; if (fcElement != null) { @@ -156,7 +171,7 @@ private void WireUpTargetElement() } #endif - //if the element is a dependency object, and not a FrameworkElement, nothing is holding onto the reference after the DelayedRegionCreationBehavior + //if the element is a dependency/avalonia object, and not a FrameworkElement/Avalonia.Control, nothing is holding onto the reference after the DelayedRegionCreationBehavior //is instantiated inside RegionManager.CreateRegion(DependencyObject element). If the GC runs before RegionManager.UpdateRegions is called, the region will //never get registered because it is gone from the updatingRegionsListeners list inside RegionManager. So we need to hold on to it. This should be rare. DependencyObject depObj = TargetElement as DependencyObject; @@ -172,11 +187,15 @@ private void UnWireTargetElement() FrameworkElement element = TargetElement as FrameworkElement; if (element != null) { +#if !AVALONIA element.Loaded -= ElementLoaded; +#else + element.AttachedToVisualTree -= ElementLoaded; +#endif return; } -#if !UNO_WINUI +#if (!UNO_WINUI && !AVALONIA) FrameworkContentElement fcElement = TargetElement as FrameworkContentElement; if (fcElement != null) { @@ -193,7 +212,6 @@ private void UnWireTargetElement() } } - /// /// Add the instance of this class to to keep it alive /// diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs index 69a6368a79..8b0c9c44be 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs @@ -1,4 +1,9 @@ using System.ComponentModel; +#if AVALONIA +using Avalonia; +using Avalonia.Controls; +using Avalonia.VisualTree; +#endif using Prism.Properties; namespace Prism.Navigation.Regions.Behaviors @@ -49,6 +54,7 @@ public DependencyObject HostControl { throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach); } + hostControl = value; } } @@ -133,6 +139,8 @@ private IRegionManager FindRegionManager(DependencyObject dependencyObject) DependencyObject parent = null; #if UNO_WINUI parent = VisualTreeHelper.GetParent(dependencyObject); +#elif AVALONIA + parent = ((dependencyObject as Avalonia.Visual)?.GetVisualParent() ?? null) as Avalonia.AvaloniaObject; #else parent = LogicalTreeHelper.GetParent(dependencyObject); #endif diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs index 056e71e229..314ecdeb92 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs @@ -8,7 +8,7 @@ namespace Prism.Navigation.Regions.Behaviors /// the control that hosts the Region. It does this by setting the /// Dependency Property on the host control. /// - /// This behavior allows the usage of two way databinding of the RegionContext from XAML. + /// This behavior allows the usage of two way data binding of the RegionContext from XAML. /// public class SyncRegionContextWithHostBehavior : RegionBehavior, IHostAwareRegionBehavior { @@ -47,6 +47,7 @@ public DependencyObject HostControl { throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach); } + _hostControl = value; } } @@ -67,7 +68,7 @@ protected override void OnAttach() } } - void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == RegionContextPropertyName) { @@ -80,7 +81,7 @@ void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChanged } } - void RegionContextObservableObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private void RegionContextObservableObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == "Value") { diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/ContentControlRegionAdapter.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/ContentControlRegionAdapter.cs index 52aa7a991c..012db99b5b 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/ContentControlRegionAdapter.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/ContentControlRegionAdapter.cs @@ -29,7 +29,11 @@ protected override void Adapt(IRegion region, ContentControl regionTarget) throw new ArgumentNullException(nameof(regionTarget)); bool contentIsSet = regionTarget.Content != null; +#if !AVALONIA contentIsSet = contentIsSet || regionTarget.HasBinding(ContentControl.ContentProperty); +#else + contentIsSet = contentIsSet || regionTarget[ContentControl.ContentProperty] != null; +#endif if (contentIsSet) throw new InvalidOperationException(Resources.ContentControlHasContentException); diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/INavigationAware.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/INavigationAware.cs index 10dedf0c5a..276384b088 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/INavigationAware.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/INavigationAware.cs @@ -1,13 +1,13 @@ -#if WPF +#if (WPF || AVALONIA) -// NOTE: This is for Legacy support for WPF apps only +// NOTE: This is for Legacy support for WPF/Avalonia apps only namespace Prism.Navigation.Regions { /// /// Provides a way for objects involved in navigation to be notified of navigation activities. /// /// - /// Provides compatibility for Legacy Prism.Wpf apps. + /// Provides compatibility for Legacy Prism.Wpf and Prism.Avalonia apps. /// public interface INavigationAware : IRegionAware { diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/IRegionManagerAccessor.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/IRegionManagerAccessor.cs index cc032df749..3c3066e88a 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/IRegionManagerAccessor.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/IRegionManagerAccessor.cs @@ -18,13 +18,21 @@ public interface IRegionManagerAccessor /// The object to adapt. This is typically a container (i.e a control). /// The name of the region that should be created when /// the RegionManager is also set in this element. +#if !AVALONIA string GetRegionName(DependencyObject element); +#else + string GetRegionName(AvaloniaObject element); +#endif /// /// Gets the value of the RegionName attached property. /// /// The target element. /// The attached to the element. +#if !AVALONIA IRegionManager GetRegionManager(DependencyObject element); +#else + IRegionManager GetRegionManager(AvaloniaObject element); +#endif } } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/ItemsControlRegionAdapter.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/ItemsControlRegionAdapter.cs index 6ff23fc583..dfb3327b2d 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/ItemsControlRegionAdapter.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/ItemsControlRegionAdapter.cs @@ -30,6 +30,8 @@ protected override void Adapt(IRegion region, ItemsControl regionTarget) if (regionTarget == null) throw new ArgumentNullException(nameof(regionTarget)); + // NOTE: In Avalonia, regionTarget.ItemsSource will not be null. Keep it rollin' baby! +#if !AVALONIA bool itemsSourceIsSet = regionTarget.ItemsSource != null; itemsSourceIsSet = itemsSourceIsSet || regionTarget.HasBinding(ItemsControl.ItemsSourceProperty); @@ -50,6 +52,31 @@ protected override void Adapt(IRegion region, ItemsControl regionTarget) } regionTarget.ItemsSource = region.Views; +#else + // If control has child items, move them to the region and then bind control to region. Can't set ItemsSource if child items exist. + if (regionTarget.ItemCount > 0) + { + foreach (object childItem in regionTarget.Items) + region.Add(childItem); + + // Control must be empty before setting ItemsSource + regionTarget.Items.Clear(); + } + + // Detect when an item has been added/removed to the ItemsControl's backing region. Copy + // all items to a new collection and bind to the region's ItemsSource + region.Views.CollectionChanged += (s, e) => + { + var enumerator = region.Views.GetEnumerator(); + List items = new(); + while (enumerator.MoveNext()) + { + items.Add(enumerator.Current); + } + + regionTarget.ItemsSource = items; + }; +#endif } /// diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Region.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Region.cs index 624f49fb24..b06acb39e1 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Region.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Region.cs @@ -3,6 +3,10 @@ using System.Globalization; using Prism.Properties; +#if AVALONIA +using Avalonia; +#endif + namespace Prism.Navigation.Regions { /// @@ -213,7 +217,11 @@ protected virtual ObservableCollection ItemMetadataCollection /// Adds a new view to the region. /// /// The view to add. +#if !AVALONIA /// The that is set on the view if it is a . It will be the current region manager when using this overload. +#else + /// The that is set on the view if it is a . It will be the current region manager when using this overload. +#endif public IRegionManager Add(string viewName) { var view = ContainerLocator.Container.Resolve(viewName); @@ -225,7 +233,12 @@ public IRegionManager Add(string viewName) /// Adds a new view to the region. /// /// The view to add. + +#if !AVALONIA /// The that is set on the view if it is a . It will be the current region manager when using this overload. +#else + /// The that is set on the view if it is a . It will be the current region manager when using this overload. +#endif public IRegionManager Add(object view) { return Add(view, null, false); @@ -236,7 +249,12 @@ public IRegionManager Add(object view) /// /// The view to add. /// The name of the view. This can be used to retrieve it later by calling . + +#if !AVALONIA /// The that is set on the view if it is a . It will be the current region manager when using this overload. +#else + /// The that is set on the view if it is a . It will be the current region manager when using this overload. +#endif public IRegionManager Add(object view, string viewName) { if (string.IsNullOrEmpty(viewName)) @@ -253,7 +271,12 @@ public IRegionManager Add(object view, string viewName) /// The view to add. /// The name of the view. This can be used to retrieve it later by calling . /// When , the added view will receive a new instance of , otherwise it will use the current region manager for this region. + +#if !AVALONIA /// The that is set on the view if it is a . +#else + /// The that is set on the view if it is a . +#endif public virtual IRegionManager Add(object view, string viewName, bool createRegionManagerScope) { IRegionManager manager = createRegionManagerScope ? RegionManager.CreateRegionManager() : RegionManager; @@ -271,10 +294,17 @@ public virtual void Remove(object view) ItemMetadataCollection.Remove(itemMetadata); +#if !AVALONIA if (view is DependencyObject dependencyObject && Regions.RegionManager.GetRegionManager(dependencyObject) == RegionManager) { dependencyObject.ClearValue(Regions.RegionManager.RegionManagerProperty); } +#else + if (view is AvaloniaObject avaloniaObject && Regions.RegionManager.GetRegionManager(avaloniaObject) == RegionManager) + { + avaloniaObject.ClearValue(Regions.RegionManager.RegionManagerProperty); + } +#endif } /// @@ -373,14 +403,21 @@ private void InnerAdd(object view, string viewName, IRegionManager scopedRegionM { throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.RegionViewNameExistsException, viewName)); } + itemMetadata.Name = viewName; } - +#if !AVALONIA if (view is DependencyObject dependencyObject) { Regions.RegionManager.SetRegionManager(dependencyObject, scopedRegionManager); } +#else + if (view is AvaloniaObject avaloniaObject) + { + Regions.RegionManager.SetRegionManager(avaloniaObject, scopedRegionManager); + } +#endif ItemMetadataCollection.Add(itemMetadata); } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionAdapterMappings.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionAdapterMappings.cs index 106d58131d..0186f287c6 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionAdapterMappings.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionAdapterMappings.cs @@ -73,8 +73,10 @@ public IRegionAdapter GetMapping(Type controlType) { return mappings[currentType]; } + currentType = currentType.BaseType; } + throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, Resources.NoRegionAdapterException, controlType)); } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionContext.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionContext.cs index 95a4e1a5b9..ffc04a595a 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionContext.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionContext.cs @@ -1,29 +1,38 @@ using Prism.Common; +using Prism.Navigation.Regions.Behaviors; +#if AVALONIA +using BindRegionContextToDependencyObjectBehavior = Prism.Navigation.Regions.Behaviors.BindRegionContextToAvaloniaObjectBehavior; +#endif namespace Prism.Navigation.Regions { /// - /// Class that holds methods to Set and Get the RegionContext from a DependencyObject. + /// Class that holds methods to Set and Get the RegionContext from a . /// /// RegionContext allows sharing of contextual information between the view that's hosting a /// and any views that are inside the Region. /// public static class RegionContext { +#if !AVALONIA private static readonly DependencyProperty ObservableRegionContextProperty = DependencyProperty.RegisterAttached("ObservableRegionContext", typeof(ObservableObject), typeof(RegionContext), null); +#else + private static readonly AvaloniaProperty ObservableRegionContextProperty = + AvaloniaProperty.RegisterAttached>("ObservableRegionContext", typeof(RegionContext)); +#endif /// /// Returns an wrapper around the RegionContext value. The RegionContext /// will be set on any views (dependency objects) that are inside the collection by - /// the Behavior. + /// the Behavior. /// The RegionContext will also be set to the control that hosts the Region, by the Behavior. /// /// If the wrapper does not already exist, an empty one will be created. This way, an observer can /// notify when the value is set for the first time. /// - /// Any view that hold the RegionContext value. - /// Wrapper around the value. + /// Any view that hold the RegionContext value. + /// Wrapper around the value. public static ObservableObject GetObservableContext(DependencyObject view) { if (view == null) @@ -39,6 +48,5 @@ public static ObservableObject GetObservableContext(DependencyObject vie return context; } - } } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs index 4ac125e9e5..d618967baf 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs @@ -34,11 +34,17 @@ public class RegionManager : IRegionManager /// will create and adapt a new region for that control, and register it /// in the with the specified region name. /// +#if !AVALONIA public static readonly DependencyProperty RegionNameProperty = DependencyProperty.RegisterAttached( "RegionName", typeof(string), typeof(RegionManager), new PropertyMetadata(defaultValue: null, propertyChangedCallback: OnSetRegionNameCallback)); +#else + public static readonly AvaloniaProperty RegionNameProperty = AvaloniaProperty.RegisterAttached( + "RegionName", + typeof(RegionManager)); +#endif /// /// Sets the attached property. @@ -67,9 +73,13 @@ public static string GetRegionName(DependencyObject regionTarget) return regionTarget.GetValue(RegionNameProperty) as string; } +#if !AVALONIA private static readonly DependencyProperty ObservableRegionProperty = - DependencyProperty.RegisterAttached("ObservableRegion", typeof(ObservableObject), typeof(RegionManager), null); - + DependencyProperty.RegisterAttached("ObservableRegion", typeof(ObservableObject), typeof(RegionManager), null); +#else + private static readonly AvaloniaProperty ObservableRegionProperty = + AvaloniaProperty.RegisterAttached>("ObservableRegion", typeof(RegionManager)); +#endif /// /// Returns an wrapper that can hold an . Using this wrapper @@ -123,8 +133,13 @@ private static void CreateRegion(DependencyObject element) /// will create and adapt a new region for that control, and register it /// in the with the specified region name. /// +#if !AVALONIA public static readonly DependencyProperty RegionManagerProperty = DependencyProperty.RegisterAttached("RegionManager", typeof(IRegionManager), typeof(RegionManager), null); +#else + public static readonly AvaloniaProperty RegionManagerProperty = + AvaloniaProperty.RegisterAttached("RegionManager", typeof(RegionManager)); +#endif /// /// Gets the value of the attached property. @@ -155,8 +170,13 @@ public static void SetRegionManager(DependencyObject target, IRegionManager valu /// /// Identifies the RegionContext attached property. /// +#if !AVALONIA public static readonly DependencyProperty RegionContextProperty = - DependencyProperty.RegisterAttached("RegionContext", typeof(object), typeof(RegionManager), new PropertyMetadata(defaultValue: null, propertyChangedCallback: OnRegionContextChanged)); + DependencyProperty.RegisterAttached("RegionContext", typeof(object), typeof(RegionManager), new PropertyMetadata(defaultValue: null, propertyChangedCallback: OnRegionContextChanged)); +#else + public static readonly AvaloniaProperty RegionContextProperty = + AvaloniaProperty.RegisterAttached("RegionContext", typeof(RegionManager)); +#endif private static void OnRegionContextChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) { @@ -227,7 +247,11 @@ public static void UpdateRegions() private static bool IsInDesignMode(DependencyObject element) { +#if !AVALONIA return DesignerProperties.GetIsInDesignMode(element); +#else + return Design.IsDesignMode; +#endif } #endregion @@ -242,6 +266,15 @@ public RegionManager() regionCollection = new RegionCollection(this); } +#if AVALONIA + static RegionManager() + { + // TODO: Could this go into the default constructor? + RegionNameProperty.Changed.Subscribe(args => OnSetRegionNameCallback(args?.Sender, args)); + RegionContextProperty.Changed.Subscribe(args => OnRegionContextChanged(args?.Sender, args)); + } +#endif + /// /// Gets a collection of that identify each region by name. You can use this collection to add or remove regions to the current region manager. /// @@ -361,12 +394,12 @@ public void RequestNavigate(string regionName, Uri source, Action - /// This method allows an IRegionManager to locate a specified region and navigate in it to the specified target Uri, passing a navigation callback and an instance of NavigationParameters, which holds a collection of object parameters. + /// This method allows an IRegionManager to locate a specified region and navigate in it to the specified target Uri, passing a navigation callback and an instance of , which holds a collection of object parameters. /// /// The name of the region where the navigation will occur. /// A Uri that represents the target where the region will navigate. /// The navigation callback that will be executed after the navigation is completed. - /// An instance of NavigationParameters, which holds a collection of object parameters. + /// An instance of , which holds a collection of object parameters. public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters) { if (navigationCallback == null) @@ -515,8 +548,7 @@ public void Add(string regionName, IRegion region) if (region.Name != null && region.Name != regionName) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.RegionManagerWithDifferentNameException, region.Name, regionName), nameof(regionName)); - if (region.Name == null) - region.Name = regionName; + region.Name ??= regionName; Add(region); } @@ -528,12 +560,7 @@ private IRegion GetRegionByName(string regionName) private void OnCollectionChanged(NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) { - var handler = CollectionChanged; - - if (handler != null) - { - handler(this, notifyCollectionChangedEventArgs); - } + CollectionChanged?.Invoke(this, notifyCollectionChangedEventArgs); } } } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionNavigationContentLoader.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionNavigationContentLoader.cs index 7996c89e9d..3d82030c8d 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionNavigationContentLoader.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionNavigationContentLoader.cs @@ -65,7 +65,6 @@ public object LoadContent(IRegion region, NavigationContext navigationContext) return navigationAware == null || navigationAware.IsNavigationTarget(navigationContext); }); - var view = acceptingCandidates.FirstOrDefault(); if (view != null) diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/SelectorRegionAdapter.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/SelectorRegionAdapter.cs index 87bbd7ce6c..29b92c0362 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/SelectorRegionAdapter.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/SelectorRegionAdapter.cs @@ -2,6 +2,7 @@ namespace Prism.Navigation.Regions { +#if !AVALONIA /// /// Adapter that creates a new and binds all /// the views to the adapted . @@ -60,4 +61,5 @@ protected override IRegion CreateRegion() return new Region(); } } +#endif } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/SingleActiveRegion.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/SingleActiveRegion.cs index 45f0b299f1..a435c77ceb 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/SingleActiveRegion.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/SingleActiveRegion.cs @@ -19,6 +19,7 @@ public override void Activate(object view) { base.Deactivate(currentActiveView); } + base.Activate(view); } } diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/ViewsCollection.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/ViewsCollection.cs index 57e956a433..cfce101d3e 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/ViewsCollection.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/ViewsCollection.cs @@ -95,8 +95,7 @@ IEnumerator IEnumerable.GetEnumerator() /// private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - NotifyCollectionChangedEventHandler handler = CollectionChanged; - if (handler != null) handler(this, e); + CollectionChanged?.Invoke(this, e); } private void NotifyReset() diff --git a/src/Wpf/Prism.Wpf/PrismInitializationExtensions.cs b/src/Wpf/Prism.Wpf/PrismInitializationExtensions.cs index 62b12ae241..e607e7dea0 100644 --- a/src/Wpf/Prism.Wpf/PrismInitializationExtensions.cs +++ b/src/Wpf/Prism.Wpf/PrismInitializationExtensions.cs @@ -43,7 +43,11 @@ internal static void RegisterRequiredTypes(this IContainerRegistry containerRegi internal static void RegisterDefaultRegionBehaviors(this IRegionBehaviorFactory regionBehaviors) { +#if AVALONIA + regionBehaviors.AddIfMissing(BindRegionContextToAvaloniaObjectBehavior.BehaviorKey); +#else regionBehaviors.AddIfMissing(BindRegionContextToDependencyObjectBehavior.BehaviorKey); +#endif regionBehaviors.AddIfMissing(RegionActiveAwareBehavior.BehaviorKey); regionBehaviors.AddIfMissing(SyncRegionContextWithHostBehavior.BehaviorKey); regionBehaviors.AddIfMissing(RegionManagerRegistrationBehavior.BehaviorKey); @@ -55,7 +59,9 @@ internal static void RegisterDefaultRegionBehaviors(this IRegionBehaviorFactory internal static void RegisterDefaultRegionAdapterMappings(this RegionAdapterMappings regionAdapterMappings) { +#if !AVALONIA regionAdapterMappings.RegisterMapping(); +#endif regionAdapterMappings.RegisterMapping(); regionAdapterMappings.RegisterMapping(); #if UNO_WINUI diff --git a/tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs b/tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs new file mode 100644 index 0000000000..4144973396 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Prism.Avalonia.Tests +{ + public class CollectionChangedTracker + { + private readonly List eventList = new List(); + + public CollectionChangedTracker(INotifyCollectionChanged collection) + { + collection.CollectionChanged += OnCollectionChanged; + } + + public IEnumerable ActionsFired { get { return this.eventList.Select(e => e.Action); } } + public IEnumerable NotifyEvents { get { return this.eventList; } } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + this.eventList.Add(e); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs new file mode 100644 index 0000000000..aecadd0bc5 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Xunit; + +namespace Prism.Avalonia.Tests +{ + + public class CollectionExtensionsFixture + { + [Fact] + public void CanAddRangeToCollection() + { + Collection col = new Collection(); + List itemsToAdd = new List { "1", "2" }; + + col.AddRange(itemsToAdd); + + Assert.Equal(2, col.Count); + Assert.Equal("1", col[0]); + Assert.Equal("2", col[1]); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs new file mode 100644 index 0000000000..ab7ace0618 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs @@ -0,0 +1,195 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.CSharp; +using Prism.Ioc; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests +{ + // Warning: + // CompileCode() method is not supported by .NET 6 + // DotNet Runtime Issue 18768 - https://github.com/dotnet/runtime/issues/18768#issuecomment-265381303 + // Workaround: Upgrade to RosylnCompiler (https://github.com/npolyak/RoslynAssembly) + public class CompilerHelper + { + private static string moduleTemplate = + @"using System; + using Prism.Ioc; + using Prism.Modularity; + namespace TestModules + { + #module# + public class #className#Class : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + Console.WriteLine(""#className#.Start""); + } + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + }"; + + public static Assembly CompileFileAndLoadAssembly(string input, string output, params string[] references) + { + return CompileFile(input, output, references).CompiledAssembly; + } + + public static CompilerResults CompileFile(string input, string output, params string[] references) + { + CreateOutput(output); + + List referencedAssemblies = new List(references.Length + 3); + + referencedAssemblies.AddRange(references); + referencedAssemblies.Add("System.dll"); + referencedAssemblies.Add(typeof(IContainerRegistry).Assembly.CodeBase.Replace(@"file:///", "")); + referencedAssemblies.Add(typeof(IModule).Assembly.CodeBase.Replace(@"file:///", "")); + referencedAssemblies.Add(typeof(ModuleAttribute).Assembly.CodeBase.Replace(@"file:///", "")); + + CSharpCodeProvider codeProvider = new CSharpCodeProvider(); + CompilerParameters cp = new CompilerParameters(referencedAssemblies.ToArray(), output); + + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(input); + + if (stream == null) + { + throw new ArgumentException("input"); + } + + StreamReader reader = new StreamReader(stream); + string source = reader.ReadToEnd(); + CompilerResults results = codeProvider.CompileAssemblyFromSource(cp, source); + ThrowIfCompilerError(results); + return results; + } + + public static void CreateOutput(string output) + { + string dir = Path.GetDirectoryName(output); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + else + { + //Delete the file if exists + if (File.Exists(output)) + { + try + { + File.Delete(output); + } + catch (UnauthorizedAccessException) + { + //The file might be locked by Visual Studio, so rename it + if (File.Exists(output + ".locked")) + File.Delete(output + ".locked"); + File.Move(output, output + ".locked"); + } + } + } + } + + public static CompilerResults CompileCode(string code, string output) + { + CreateOutput(output); + List referencedAssemblies = new List(); + referencedAssemblies.Add("System.dll"); + referencedAssemblies.Add(typeof(IContainerExtension).Assembly.CodeBase.Replace(@"file:///", "")); + referencedAssemblies.Add(typeof(IModule).Assembly.CodeBase.Replace(@"file:///", "")); + referencedAssemblies.Add(typeof(ModuleAttribute).Assembly.CodeBase.Replace(@"file:///", "")); + + CompilerResults results = new CSharpCodeProvider().CompileAssemblyFromSource( + new CompilerParameters(referencedAssemblies.ToArray(), output), code); + + ThrowIfCompilerError(results); + + return results; + } + + public static string GenerateDynamicModule(string assemblyName, string moduleName, string outpath, params string[] dependencies) + { + CreateOutput(outpath); + + // Create temporary module. + string moduleCode = moduleTemplate.Replace("#className#", assemblyName); + if (!string.IsNullOrEmpty(moduleName)) + { + moduleCode = moduleCode.Replace("#module#", String.Format("[Module(ModuleName = \"{0}\") #dependencies#]", moduleName)); + } + else + { + moduleCode = moduleCode.Replace("#module#", ""); + } + + string depString = string.Empty; + + foreach (string module in dependencies) + { + depString += String.Format(", ModuleDependency(\"{0}\")", module); + } + + moduleCode = moduleCode.Replace("#dependencies#", depString); + + CompileCode(moduleCode, outpath); + + return outpath; + } + + public static string GenerateDynamicModule(string assemblyName, string moduleName, params string[] dependencies) + { + string assemblyFile = assemblyName + ".dll"; + string outpath = Path.Combine(assemblyName, assemblyFile); + + return GenerateDynamicModule(assemblyName, moduleName, outpath, dependencies); + } + + public static void ThrowIfCompilerError(CompilerResults results) + { + if (results.Errors.HasErrors) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Compilation failed."); + foreach (CompilerError error in results.Errors) + { + sb.AppendLine(error.ToString()); + } + + Assert.False(results.Errors.HasErrors, sb.ToString()); + } + } + + public static void CleanUpDirectory(string path) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + else + { + foreach (string file in Directory.GetFiles(path)) + { + try + { + File.Delete(file); + } + catch (UnauthorizedAccessException) + { + //The file might be locked by Visual Studio, so rename it + if (File.Exists(file + ".locked")) + File.Delete(file + ".locked"); + File.Move(file, file + ".locked"); + } + } + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs b/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs new file mode 100644 index 0000000000..8601764bc8 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs @@ -0,0 +1,29 @@ +using System; +using Xunit; + +namespace Prism.Avalonia.Tests +{ + public static class ExceptionAssert + { + public static void Throws(Action action) + where TException : Exception + { + Throws(typeof(TException), action); + } + + public static void Throws(Type expectedExceptionType, Action action) + { + try + { + action(); + } + catch (Exception ex) + { + Assert.IsType(expectedExceptionType, ex); + return; + } + + //Assert.Fail("No exception thrown. Expected exception type of {0}.", expectedExceptionType.Name); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs new file mode 100644 index 0000000000..4a46c6910f --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs @@ -0,0 +1,169 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls; +using Prism.Interactivity; +using Xunit; + +namespace Prism.Avalonia.Tests.Interactivity +{ + + public class CommandBehaviorBaseFixture + { + [Fact] + public void ExecuteUsesCommandParameterWhenSet() + { + var targetUIElement = new Control(); + var target = new TestableCommandBehaviorBase(targetUIElement); + target.CommandParameter = "123"; + TestCommand testCommand = new TestCommand(); + target.Command = testCommand; + + target.ExecuteCommand("testparam"); + + Assert.Equal("123", testCommand.ExecuteCalledWithParameter); + } + + [Fact] + public void ExecuteUsesParameterWhenCommandParameterNotSet() + { + var targetUIElement = new Control(); + var target = new TestableCommandBehaviorBase(targetUIElement); + TestCommand testCommand = new TestCommand(); + target.Command = testCommand; + + target.ExecuteCommand("testparam"); + + Assert.Equal("testparam", testCommand.ExecuteCalledWithParameter); + } + + [Fact] + public void CommandBehaviorBaseAllowsDisableByDefault() + { + var targetUIElement = new Control(); + var target = new TestableCommandBehaviorBase(targetUIElement); + + Assert.True(target.AutoEnable); + } + + [StaFact] + public void CommandBehaviorBaseEnablesUIElement() + { + var targetUIElement = new Control(); + targetUIElement.IsEnabled = false; + + var target = new TestableCommandBehaviorBase(targetUIElement); + TestCommand testCommand = new TestCommand(); + target.Command = testCommand; + target.ExecuteCommand(null); + + Assert.True(targetUIElement.IsEnabled); + } + + [StaFact] + public void CommandBehaviorBaseDisablesUIElement() + { + var targetUIElement = new Control(); + targetUIElement.IsEnabled = true; + + var target = new TestableCommandBehaviorBase(targetUIElement); + TestCommand testCommand = new TestCommand(); + testCommand.CanExecuteResult = false; + target.Command = testCommand; + target.ExecuteCommand(null); + + Assert.False(targetUIElement.IsEnabled); + } + + [StaFact] + public void WhenAutoEnableIsFalse_ThenDisabledUIElementRemainsDisabled() + { + var targetUIElement = new Control(); + targetUIElement.IsEnabled = false; + + var target = new TestableCommandBehaviorBase(targetUIElement); + target.AutoEnable = false; + TestCommand testCommand = new TestCommand(); + target.Command = testCommand; + target.ExecuteCommand(null); + + Assert.False(targetUIElement.IsEnabled); + } + + [StaFact] + public void WhenAutoEnableIsUpdated_ThenDisabledUIElementIsEnabled() + { + var targetUIElement = new Control(); + targetUIElement.IsEnabled = false; + + var target = new TestableCommandBehaviorBase(targetUIElement); + target.AutoEnable = false; + TestCommand testCommand = new TestCommand(); + target.Command = testCommand; + target.ExecuteCommand(null); + + Assert.False(targetUIElement.IsEnabled); + + target.AutoEnable = true; + + Assert.True(targetUIElement.IsEnabled); + } + + [StaFact] + public void WhenAutoEnableIsUpdated_ThenEnabledUIElementIsDisabled() + { + var targetUIElement = new Control(); + targetUIElement.IsEnabled = true; + + var target = new TestableCommandBehaviorBase(targetUIElement); + target.AutoEnable = false; + TestCommand testCommand = new TestCommand(); + testCommand.CanExecuteResult = false; + target.Command = testCommand; + target.ExecuteCommand(null); + + Assert.True(targetUIElement.IsEnabled); + + target.AutoEnable = true; + + Assert.False(targetUIElement.IsEnabled); + } + } + + class TestableCommandBehaviorBase : CommandBehaviorBase + { + public TestableCommandBehaviorBase(Control targetObject) + : base(targetObject) + { } + + public new void ExecuteCommand(object parameter) + { + base.ExecuteCommand(parameter); + } + } + + class TestCommand : ICommand + { + bool _canExecte = true; + public bool CanExecuteResult + { + get { return _canExecte; } + set { _canExecte = value; } + } + + public object CanExecuteCalledWithParameter { get; set; } + + public bool CanExecute(object parameter) + { + CanExecuteCalledWithParameter = parameter; + return CanExecuteResult; + } + + public event EventHandler CanExecuteChanged; + + public object ExecuteCalledWithParameter { get; set; } + public void Execute(object parameter) + { + ExecuteCalledWithParameter = parameter; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs new file mode 100644 index 0000000000..1e5051971b --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs @@ -0,0 +1,384 @@ +using System; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Prism.Avalonia.Tests.Mocks; +using Xunit; + +namespace Prism.Avalonia.Tests.Interactivity +{ + // Override Prism.Interactivity.InvokeCommandAction until it can be implemented. + //// Reference: + //// https://github.com/wieslawsoltes/AvaloniaBehaviors/blob/master/src/Avalonia.Xaml.Interactions/Core/InvokeCommandAction.cs + public class InvokeCommandAction : AvaloniaObject + { + /// + /// Dependency property identifying if the associated element should automatically be enabled or disabled based on the result of the Command's CanExecute + /// + public static readonly StyledProperty AutoEnableProperty = + AvaloniaProperty.Register(nameof(AutoEnable)); + + /// Identifies the avalonia property. + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command)); + + /// Identifies the avalonia property. + public static readonly StyledProperty CommandParameterProperty = + AvaloniaProperty.Register(nameof(CommandParameter)); + + /// + /// Dependency property identifying the TriggerParameterPath to be parsed to identify the child property of the trigger parameter to be used as the command parameter. + /// + public static readonly StyledProperty TriggerParameterPathProperty = + AvaloniaProperty.Register(nameof(TriggerParameterPath)); + + /// + /// Gets or sets whether or not the associated element will automatically be enabled or disabled based on the result of the commands CanExecute + /// + public bool AutoEnable + { + get { return (bool)GetValue(AutoEnableProperty); } + set { SetValue(AutoEnableProperty, value); } + } + + public ICommand? Command { get; set; } + + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + public string TriggerParameterPath + { + get => GetValue(TriggerParameterPathProperty) as string; + set => SetValue(TriggerParameterPathProperty, value); + } + + public void Attach(Control? ctrl) + { } + + public void Detach() + { } + + public void InvokeAction(object? action) + { } + } + + public class InvokeCommandActionFixture + { + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCommandPropertyIsSet_ThenHooksUpCommandBehavior() + { + var someControl = new TextBox(); + var commandAction = new InvokeCommandAction(); + var command = new MockCommand(); + commandAction.Attach(someControl); + commandAction.Command = command; + + Assert.False(command.ExecuteCalled); + + commandAction.InvokeAction(null); + + Assert.True(command.ExecuteCalled); + Assert.Same(command, commandAction.GetValue(InvokeCommandAction.CommandProperty)); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAttachedAfterCommandPropertyIsSetAndInvoked_ThenInvokesCommand() + { + var someControl = new TextBox(); + var commandAction = new InvokeCommandAction(); + var command = new MockCommand(); + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.False(command.ExecuteCalled); + + commandAction.InvokeAction(null); + + Assert.True(command.ExecuteCalled); + Assert.Same(command, commandAction.GetValue(InvokeCommandAction.CommandProperty)); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenChangingProperty_ThenUpdatesCommand() + { + var someControl = new TextBox(); + var oldCommand = new MockCommand(); + var newCommand = new MockCommand(); + var commandAction = new InvokeCommandAction(); + commandAction.Attach(someControl); + commandAction.Command = oldCommand; + commandAction.Command = newCommand; + commandAction.InvokeAction(null); + + Assert.True(newCommand.ExecuteCalled); + Assert.False(oldCommand.ExecuteCalled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenInvokedWithCommandParameter_ThenPassesCommandParaeterToExecute() + { + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Attach(someControl); + commandAction.Command = command; + commandAction.CommandParameter = parameter; + + Assert.Null(command.ExecuteParameter); + + commandAction.InvokeAction(null); + + Assert.True(command.ExecuteCalled); + Assert.NotNull(command.ExecuteParameter); + Assert.Same(parameter, command.ExecuteParameter); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCommandParameterChanged_ThenUpdatesIsEnabledState() + { + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Attach(someControl); + commandAction.Command = command; + + Assert.Null(command.CanExecuteParameter); + Assert.True(someControl.IsEnabled); + + command.CanExecuteReturnValue = false; + commandAction.CommandParameter = parameter; + + Assert.NotNull(command.CanExecuteParameter); + Assert.Same(parameter, command.CanExecuteParameter); + Assert.False(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCanExecuteChanged_ThenUpdatesIsEnabledState() + { + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Attach(someControl); + commandAction.Command = command; + commandAction.CommandParameter = parameter; + + Assert.True(someControl.IsEnabled); + + command.CanExecuteReturnValue = false; + command.RaiseCanExecuteChanged(); + + Assert.NotNull(command.CanExecuteParameter); + Assert.Same(parameter, command.CanExecuteParameter); + Assert.False(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenDetatched_ThenSetsCommandAndCommandParameterToNull() + { + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Attach(someControl); + commandAction.Command = command; + commandAction.CommandParameter = parameter; + + Assert.NotNull(commandAction.Command); + Assert.NotNull(commandAction.CommandParameter); + + commandAction.Detach(); + + Assert.Null(commandAction.Command); + Assert.Null(commandAction.CommandParameter); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCommandIsSetAndThenBehaviorIsAttached_ThenCommandsCanExecuteIsCalledOnce() + { + var someControl = new TextBox(); + var command = new MockCommand(); + var commandAction = new InvokeCommandAction(); + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.Equal(1, command.CanExecuteTimesCalled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCommandAndCommandParameterAreSetPriorToBehaviorBeingAttached_ThenCommandIsExecutedCorrectlyOnInvoke() + { + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Command = command; + commandAction.CommandParameter = parameter; + commandAction.Attach(someControl); + + commandAction.InvokeAction(null); + + Assert.True(command.ExecuteCalled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCommandParameterNotSet_ThenEventArgsPassed() + { + var eventArgs = new TestEventArgs(null); + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Command = command; + commandAction.Attach(someControl); + + commandAction.InvokeAction(eventArgs); + + Assert.IsType(command.ExecuteParameter); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenCommandParameterNotSetAndEventArgsParameterPathSet_ThenPathedValuePassed() + { + var eventArgs = new TestEventArgs("testname"); + var someControl = new TextBox(); + var command = new MockCommand(); + var parameter = new object(); + var commandAction = new InvokeCommandAction(); + commandAction.Command = command; + commandAction.TriggerParameterPath = "Thing1.Thing2.Name"; + commandAction.Attach(someControl); + + commandAction.InvokeAction(eventArgs); + + Assert.Equal("testname", command.ExecuteParameter); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAttachedAndCanExecuteReturnsTrue_ThenDisabledUIElementIsEnabled() + { + var someControl = new TextBox(); + someControl.IsEnabled = false; + + var command = new MockCommand(); + command.CanExecuteReturnValue = true; + var commandAction = new InvokeCommandAction(); + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.True(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAttachedAndCanExecuteReturnsFalse_ThenEnabledUIElementIsDisabled() + { + var someControl = new TextBox(); + someControl.IsEnabled = true; + + var command = new MockCommand(); + command.CanExecuteReturnValue = false; + var commandAction = new InvokeCommandAction(); + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.False(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAutoEnableIsFalse_ThenDisabledUIElementRemainsDisabled() + { + var someControl = new TextBox(); + someControl.IsEnabled = false; + + var command = new MockCommand(); + command.CanExecuteReturnValue = true; + var commandAction = new InvokeCommandAction(); + commandAction.AutoEnable = false; + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.False(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAutoEnableIsFalse_ThenEnabledUIElementRemainsEnabled() + { + var someControl = new TextBox(); + someControl.IsEnabled = true; + + var command = new MockCommand(); + command.CanExecuteReturnValue = false; + var commandAction = new InvokeCommandAction(); + commandAction.AutoEnable = false; + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.True(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAutoEnableIsUpdated_ThenDisabledUIElementIsEnabled() + { + var someControl = new TextBox(); + someControl.IsEnabled = false; + + var command = new MockCommand(); + var commandAction = new InvokeCommandAction(); + commandAction.AutoEnable = false; + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.False(someControl.IsEnabled); + + commandAction.AutoEnable = true; + + Assert.True(someControl.IsEnabled); + } + + [StaFact(Skip = "InvokeCommandAction is not implmented.")] + public void WhenAutoEnableIsUpdated_ThenEnabledUIElementIsDisabled() + { + var someControl = new TextBox(); + someControl.IsEnabled = true; + + var command = new MockCommand(); + command.CanExecuteReturnValue = false; + var commandAction = new InvokeCommandAction(); + commandAction.AutoEnable = false; + commandAction.Command = command; + commandAction.Attach(someControl); + + Assert.True(someControl.IsEnabled); + + commandAction.AutoEnable = true; + + Assert.False(someControl.IsEnabled); + } + } + + internal class TestEventArgs : EventArgs + { + public TestEventArgs(string name) + { + Thing1 = new Thing1 { Thing2 = new Thing2 { Name = name } }; + } + + public Thing1 Thing1 { get; set; } + } + + internal class Thing1 + { + public Thing2 Thing2 { get; set; } + } + + internal class Thing2 + { + public string Name { get; set; } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/ObservableBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/ObservableBehaviorFixture.cs new file mode 100644 index 0000000000..1c58b61b6b --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/ObservableBehaviorFixture.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Xunit; +using Prism.Extensions; + +namespace Prism.Avalonia.Tests.Interactivity +{ + public class ObservableBehaviorFixture + { + [Fact] + public void SubscribeWorksOnIObservableWithLambda() + { + var targetUIElement = new TestAvaloniaObject(); + targetUIElement.Test = "123"; + + Assert.Equal("123", targetUIElement.OutTest); + } + } + + class TestAvaloniaObject : AvaloniaObject + { + /// + /// The property to subscribe on. + /// + public static readonly StyledProperty TestProperty = + AvaloniaProperty.Register("Test"); + + /// + /// Gets or sets a value to the TestProperty. + /// + public string Test + { + get { return (string)GetValue(TestProperty); } + set { SetValue(TestProperty, value); } + } + + /// + /// The out test property to check after TestProperty change + /// + public string OutTest { get; set; } + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public TestAvaloniaObject() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + TestProperty.Changed.Subscribe(args => OnPropertyChanged(args)); + } + + private void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + OutTest = args.NewValue.Value; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs new file mode 100644 index 0000000000..e882d50cbb --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using Prism.Common; +using Xunit; + +namespace Prism.Avalonia.Tests +{ + public class ListDictionaryFixture + { + static ListDictionary list; + + public ListDictionaryFixture() + { + list = new ListDictionary(); + } + + [Fact] + public void AddThrowsIfKeyNull() + { + var ex = Assert.Throws(() => + { + list.Add(null, new object()); + }); + + } + + [Fact] + public void AddThrowsIfValueNull() + { + var ex = Assert.Throws(() => + { + list.Add("", null); + }); + + } + + [Fact] + public void CanAddValue() + { + object value1 = new object(); + object value2 = new object(); + + list.Add("foo", value1); + list.Add("foo", value2); + + Assert.Equal(2, list["foo"].Count); + Assert.Same(value1, list["foo"][0]); + Assert.Same(value2, list["foo"][1]); + } + + [Fact] + public void CanIndexValuesByKey() + { + list.Add("foo", new object()); + list.Add("foo", new object()); + + Assert.Equal(2, list["foo"].Count); + } + + [Fact] + public void ThrowsIfRemoveKeyNull() + { + var ex = Assert.Throws(() => + { + list.RemoveValue(null, new object()); + }); + + } + + [Fact] + public void CanRemoveValue() + { + object value = new object(); + + list.Add("foo", value); + list.RemoveValue("foo", value); + + Assert.Equal(0, list["foo"].Count); + } + + [Fact] + public void CanRemoveValueFromAllLists() + { + object value = new object(); + list.Add("foo", value); + list.Add("bar", value); + + list.RemoveValue(value); + + Assert.Equal(0, list.Values.Count); + } + + [Fact] + public void RemoveNonExistingValueNoOp() + { + list.Add("foo", new object()); + + list.RemoveValue("foo", new object()); + } + + [Fact] + public void RemoveNonExistingKeyNoOp() + { + list.RemoveValue("foo", new object()); + } + + [Fact] + public void ThrowsIfRemoveListKeyNull() + { + var ex = Assert.Throws(() => + { + list.Remove(null); + }); + } + + [Fact] + public void CanRemoveList() + { + list.Add("foo", new object()); + list.Add("foo", new object()); + + bool removed = list.Remove("foo"); + + Assert.True(removed); + Assert.Equal(0, list.Keys.Count); + } + + [Fact] + public void CanSetList() + { + List values = new List(); + values.Add(new object()); + list.Add("foo", new object()); + list.Add("foo", new object()); + + list["foo"] = values; + + Assert.Equal(1, list["foo"].Count); + } + + [Fact] + public void CanEnumerateKeyValueList() + { + int count = 0; + list.Add("foo", new object()); + list.Add("foo", new object()); + + foreach (KeyValuePair> pair in list) + { + foreach (object value in pair.Value) + { + count++; + } + + Assert.Equal("foo", pair.Key); + } + + Assert.Equal(2, count); + } + + [Fact] + public void CanGetFlatListOfValues() + { + list.Add("foo", new object()); + list.Add("foo", new object()); + list.Add("bar", new object()); + + IList values = list.Values; + + Assert.Equal(3, values.Count); + } + + [Fact] + public void IndexerAccessAlwaysSucceeds() + { + IList values = list["foo"]; + + Assert.NotNull(values); + } + + [Fact] + public void ThrowsIfContainsKeyNull() + { + var ex = Assert.Throws(() => + { + list.ContainsKey(null); + }); + } + + [Fact] + public void CanAskContainsKey() + { + Assert.False(list.ContainsKey("foo")); + } + + [Fact] + public void CanAskContainsValueInAnyList() + { + object obj = new object(); + list.Add("foo", new object()); + list.Add("bar", new object()); + list.Add("baz", obj); + + bool contains = list.ContainsValue(obj); + + Assert.True(contains); + } + + [Fact] + public void CanClearDictionary() + { + list.Add("foo", new object()); + list.Add("bar", new object()); + list.Add("baz", new object()); + + list.Clear(); + + Assert.Empty(list); + } + + [Fact] + public void CanGetFilteredValuesByKeys() + { + list.Add("foo", new object()); + list.Add("bar", new object()); + list.Add("baz", new object()); + + IEnumerable filtered = list.FindAllValuesByKey(delegate (string key) + { + return key.StartsWith("b"); + }); + + int count = 0; + foreach (object obj in filtered) + { + count++; + } + + Assert.Equal(2, count); + } + + [Fact] + public void CanGetFilteredValues() + { + list.Add("foo", DateTime.Now); + list.Add("bar", new object()); + list.Add("baz", DateTime.Today); + + IEnumerable filtered = list.FindAllValues(delegate (object value) + { + return value is DateTime; + }); + int count = 0; + foreach (object obj in filtered) + { + count++; + } + + Assert.Equal(2, count); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockAsyncModuleTypeLoader.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockAsyncModuleTypeLoader.cs new file mode 100644 index 0000000000..8a1c33c84e --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockAsyncModuleTypeLoader.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks +{ + public class MockAsyncModuleTypeLoader : IModuleTypeLoader + { + private ManualResetEvent callbackEvent; + + public MockAsyncModuleTypeLoader(ManualResetEvent callbackEvent) + { + this.callbackEvent = callbackEvent; + } + + public int SleepTimeOut { get; set; } + + public Exception CallbackArgumentError { get; set; } + + public bool CanLoadModuleType(IModuleInfo moduleInfo) + { + return true; + } + + public void LoadModuleType(IModuleInfo moduleInfo) + { + Thread retrieverThread = new Thread(() => + { + Thread.Sleep(SleepTimeOut); + + RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, CallbackArgumentError)); + callbackEvent.Set(); + }); + retrieverThread.Start(); + } + + public event EventHandler ModuleDownloadProgressChanged; + + private void RaiseLoadModuleProgressChanged(ModuleDownloadProgressChangedEventArgs e) + { + ModuleDownloadProgressChanged?.Invoke(this, e); + } + + public event EventHandler LoadModuleCompleted; + + private void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) + { + LoadModuleCompleted?.Invoke(this, e); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockClickableObject.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockClickableObject.cs new file mode 100644 index 0000000000..0c9232193b --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockClickableObject.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockClickableObject : Button // : ButtonBase + { + public void RaiseClick() + { + OnClick(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockCommand.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockCommand.cs new file mode 100644 index 0000000000..34eed4a215 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockCommand.cs @@ -0,0 +1,35 @@ +using System; +using System.Windows.Input; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockCommand : ICommand + { + public bool ExecuteCalled { get; set; } + public bool CanExecuteReturnValue = true; + public object ExecuteParameter; + public object CanExecuteParameter; + public int CanExecuteTimesCalled; + + public event EventHandler CanExecuteChanged; + + public void Execute(object parameter) + { + ExecuteCalled = true; + ExecuteParameter = parameter; + } + + public bool CanExecute(object parameter) + { + CanExecuteTimesCalled++; + CanExecuteParameter = parameter; + return CanExecuteReturnValue; + } + + public void RaiseCanExecuteChanged() + { + if (CanExecuteChanged != null) + CanExecuteChanged(this, EventArgs.Empty); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockConfigurationStore.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockConfigurationStore.Desktop.cs new file mode 100644 index 0000000000..0626541492 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockConfigurationStore.Desktop.cs @@ -0,0 +1,19 @@ +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks +{ + public class MockConfigurationStore : IConfigurationStore + { + private ModulesConfigurationSection _section = new ModulesConfigurationSection(); + + public ModuleConfigurationElement[] Modules + { + set { _section.Modules = new ModuleConfigurationElementCollection(value); } + } + + public ModulesConfigurationSection RetrieveModuleConfigurationSection() + { + return _section; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockContainerAdapter.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockContainerAdapter.cs new file mode 100644 index 0000000000..0dc7f60884 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockContainerAdapter.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using Prism.Ioc; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockContainerAdapter : IContainerExtension + { + public Dictionary ResolvedInstances = new Dictionary(); + + public IScopedProvider CurrentScope { get; } + + public void CreateScope() + { + throw new NotImplementedException(); + } + + public void FinalizeExtension() + { + + } + + public bool IsRegistered(Type type) + { + throw new NotImplementedException(); + } + + public bool IsRegistered(Type type, string name) + { + throw new NotImplementedException(); + } + + public IContainerRegistry Register(Type from, Type to) + { + throw new NotImplementedException(); + } + + public IContainerRegistry Register(Type from, Type to, string name) + { + throw new NotImplementedException(); + } + + public IContainerRegistry Register(Type type, Func factoryMethod) + { + throw new NotImplementedException(); + } + + public IContainerRegistry Register(Type type, Func factoryMethod) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterInstance(Type type, object instance) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterInstance(Type type, object instance, string name) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterMany(Type type, params Type[] serviceTypes) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterManySingleton(Type type, params Type[] serviceTypes) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterScoped(Type from, Type to) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterScoped(Type type, Func factoryMethod) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterScoped(Type type, Func factoryMethod) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterSingleton(Type from, Type to) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterSingleton(Type from, Type to, string name) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterSingleton(Type type, Func factoryMethod) + { + throw new NotImplementedException(); + } + + public IContainerRegistry RegisterSingleton(Type type, Func factoryMethod) + { + throw new NotImplementedException(); + } + + public object Resolve(Type type) + { + object resolvedInstance; + if (!ResolvedInstances.ContainsKey(type)) + { + resolvedInstance = Activator.CreateInstance(type); + ResolvedInstances.Add(type, resolvedInstance); + } + else + { + resolvedInstance = ResolvedInstances[type]; + } + + return resolvedInstance; + } + + public object Resolve(Type type, string name) + { + throw new NotImplementedException(); + } + + public object Resolve(Type type, params (Type Type, object Instance)[] parameters) + { + throw new NotImplementedException(); + } + + public object Resolve(Type type, string name, params (Type Type, object Instance)[] parameters) + { + throw new NotImplementedException(); + } + + IScopedProvider IContainerProvider.CreateScope() + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDelegateReference.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDelegateReference.cs new file mode 100644 index 0000000000..e4be357990 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDelegateReference.cs @@ -0,0 +1,19 @@ +using System; +using Prism.Events; + +namespace Prism.Avalonia.Tests.Mocks +{ + class MockDelegateReference : IDelegateReference + { + public Delegate Target { get; set; } + + public MockDelegateReference() + { + } + + public MockDelegateReference(Delegate target) + { + Target = target; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDependencyObject.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDependencyObject.cs new file mode 100644 index 0000000000..e069e3e1a9 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDependencyObject.cs @@ -0,0 +1,10 @@ +using Avalonia; + +namespace Prism.Avalonia.Tests.Mocks +{ + /// MockAvaloniaObject. + /// TODO: Rename to MockAvaloniaObject. + public class MockDependencyObject : AvaloniaObject + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkContentElement.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkContentElement.cs new file mode 100644 index 0000000000..497dce6d47 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkContentElement.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace Prism.Avalonia.Tests.Mocks +{ + /// Mock Framework Content Element + /// + /// The Avalonia.Control's LoadedEvent and UnloadedEvent will + /// arrive in Avalonia v0.11.0. + /// Discussion: https://github.com/AvaloniaUI/Avalonia/issues/7908 + /// PR: https://github.com/AvaloniaUI/Avalonia/pull/8277 + /// + public class MockFrameworkContentElement : Control + { + public void RaiseLoaded() + { + ////this.RaiseEvent(new RoutedEventArgs(LoadedEvent)); + this.RaiseEvent(new RoutedEventArgs()); + } + + public void RaiseUnloaded() + { + //// this.RaiseEvent(new RoutedEventArgs(UnloadedEvent)); + this.RaiseEvent(new RoutedEventArgs()); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkElement.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkElement.cs new file mode 100644 index 0000000000..3d71edc269 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkElement.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace Prism.Avalonia.Tests.Mocks +{ + /// Mock Content Element + /// + /// TODO: + /// The Avalonia.Control's LoadedEvent and UnloadedEvent will + /// arrive in Avalonia v0.11.0. + /// Discussion: https://github.com/AvaloniaUI/Avalonia/issues/7908 + /// PR: https://github.com/AvaloniaUI/Avalonia/pull/8277 + /// + public class MockFrameworkElement : Control + { + public void RaiseLoaded() + { + //// WPF: this.RaiseEvent(new RoutedEventArgs(LoadedEvent)); + this.RaiseEvent(new RoutedEventArgs()); + } + + public void RaiseUnloaded() + { + //// WPF: this.RaiseEvent(new RoutedEventArgs(UnloadedEvent)); + this.RaiseEvent(new RoutedEventArgs()); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockHostAwareRegionBehavior.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockHostAwareRegionBehavior.cs new file mode 100644 index 0000000000..847a7a4754 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockHostAwareRegionBehavior.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; + +namespace Prism.Avalonia.Tests.Mocks +{ + public class MockHostAwareRegionBehavior : IHostAwareRegionBehavior + { + public IRegion Region { get; set; } + + public void Attach() + { + + } + + public AvaloniaObject HostControl { get; set; } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockInteractionRequestAwareElement.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockInteractionRequestAwareElement.cs new file mode 100644 index 0000000000..927b192eec --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockInteractionRequestAwareElement.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows; +using Avalonia; +using Avalonia.Controls; +using Prism.Interactivity.InteractionRequest; + +namespace Prism.Avalonia.Tests.Mocks +{ + public class MockInteractionRequestAwareElement : StyledElement, IInteractionRequestAware + { + public INotification Notification { get; set; } + + public Action FinishInteraction { get; set; } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockModuleTypeLoader.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockModuleTypeLoader.cs new file mode 100644 index 0000000000..35324975cd --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockModuleTypeLoader.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks +{ + public class MockModuleTypeLoader : IModuleTypeLoader + { + public List LoadedModules = new List(); + public bool canLoadModuleTypeReturnValue = true; + public Exception LoadCompletedError; + + public bool CanLoadModuleType(IModuleInfo moduleInfo) + { + return canLoadModuleTypeReturnValue; + } + + public void LoadModuleType(IModuleInfo moduleInfo) + { + LoadedModules.Add(moduleInfo); + RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, LoadCompletedError)); + } + + public event EventHandler ModuleDownloadProgressChanged; + + public void RaiseLoadModuleProgressChanged(ModuleDownloadProgressChangedEventArgs e) + { + ModuleDownloadProgressChanged?.Invoke(this, e); + } + + public event EventHandler LoadModuleCompleted; + + public void RaiseLoadModuleCompleted(ModuleInfo moduleInfo, Exception error) + { + RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, error)); + } + + public void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) + { + LoadModuleCompleted?.Invoke(this, e); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockPresentationRegion.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockPresentationRegion.cs new file mode 100644 index 0000000000..6acd101525 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockPresentationRegion.cs @@ -0,0 +1,145 @@ +using System; +using System.ComponentModel; +using Prism.Navigation; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + class MockPresentationRegion : IRegion + { + public MockViewsCollection MockViews = new MockViewsCollection(); + public MockViewsCollection MockActiveViews = new MockViewsCollection(); + + public MockPresentationRegion() + { + Behaviors = new MockRegionBehaviorCollection(); + } + + public IRegionManager Add(string viewName) => throw new NotImplementedException(); + + public IRegionManager Add(object view) + { + MockViews.Items.Add(view); + + return null; + } + + public void Remove(object view) + { + MockViews.Items.Remove(view); + MockActiveViews.Items.Remove(view); + } + + public void Activate(object view) + { + MockActiveViews.Items.Add(view); + } + + public IRegionManager Add(object view, string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager Add(object view, string viewName, bool createRegionManagerScope) + { + throw new NotImplementedException(); + } + + public object GetView(string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager RegionManager { get; set; } + + public IRegionBehaviorCollection Behaviors { get; set; } + + public IViewsCollection Views + { + get { return MockViews; } + } + + public IViewsCollection ActiveViews + { + get { return MockActiveViews; } + } + + public void Deactivate(object view) + { + MockActiveViews.Items.Remove(view); + } + + private object context; + public object Context + { + get { return context; } + set + { + context = value; + OnPropertyChange("Context"); + } + } + + public INavigationParameters NavigationParameters + { + get { throw new System.NotImplementedException(); } + set { throw new System.NotImplementedException(); } + } + + private string name; + public string Name + { + get { return name; } + set + { + name = value; + OnPropertyChange("Name"); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + public void OnPropertyChange(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public bool Navigate(Uri source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(Uri target, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RemoveAll() + { + throw new NotImplementedException(); + } + + public IRegionNavigationService NavigationService + { + get { throw new NotImplementedException(); } + set { throw new System.NotImplementedException(); } + } + + public Comparison SortComparison + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegion.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegion.cs new file mode 100644 index 0000000000..54df6b09c6 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegion.cs @@ -0,0 +1,117 @@ +using System; +using System.ComponentModel; +using Prism.Navigation; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockRegion : IRegion + { + public event PropertyChangedEventHandler PropertyChanged; + + public Func GetViewStringDelegate { get; set; } + + private MockViewsCollection views = new MockViewsCollection(); + + public IViewsCollection Views + { + get { return views; } + } + + public IViewsCollection ActiveViews + { + get { throw new System.NotImplementedException(); } + } + + public object Context + { + get { throw new System.NotImplementedException(); } + set { throw new System.NotImplementedException(); } + } + + public INavigationParameters NavigationParameters + { + get { throw new System.NotImplementedException(); } + set { throw new System.NotImplementedException(); } + } + + public string Name { get; set; } + + public IRegionManager Add(string viewName) => throw new NotImplementedException(); + + public IRegionManager Add(object view) + { + views.Add(view); + return null; + } + + public IRegionManager Add(object view, string viewName) + { + return Add(view); + } + + public IRegionManager Add(object view, string viewName, bool createRegionManagerScope) + { + throw new System.NotImplementedException(); + } + + public void Remove(object view) + { + throw new System.NotImplementedException(); + } + + public void Activate(object view) + { + throw new System.NotImplementedException(); + } + + public void Deactivate(object view) + { + throw new System.NotImplementedException(); + } + + public object GetView(string viewName) + { + return GetViewStringDelegate(viewName); + } + + public IRegionManager RegionManager { get; set; } + + public IRegionBehaviorCollection Behaviors + { + get { throw new System.NotImplementedException(); } + } + + public bool Navigate(System.Uri source) + { + throw new System.NotImplementedException(); + } + + public void RequestNavigate(System.Uri target, System.Action navigationCallback) + { + throw new System.NotImplementedException(); + } + + public void RequestNavigate(System.Uri target, System.Action navigationCallback, INavigationParameters navigationParameters) + { + throw new System.NotImplementedException(); + } + + public void RemoveAll() + { + throw new NotImplementedException(); + } + + public IRegionNavigationService NavigationService + { + get { throw new System.NotImplementedException(); } + set { throw new System.NotImplementedException(); } + } + + public System.Comparison SortComparison + { + get { throw new System.NotImplementedException(); } + set { throw new System.NotImplementedException(); } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionAdapter.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionAdapter.cs new file mode 100644 index 0000000000..2a63cf1114 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionAdapter.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Avalonia; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockRegionAdapter : IRegionAdapter + { + public List CreatedRegions = new List(); + public MockRegionManagerAccessor Accessor; + + public IRegion Initialize(object regionTarget, string regionName) + { + CreatedRegions.Add(regionName); + + var region = new MockPresentationRegion(); + RegionManager.GetObservableRegion(regionTarget as AvaloniaObject).Value = region; + + // Fire update regions again. This also happens if a region is created and added to the regionmanager + if (Accessor != null) + Accessor.UpdateRegions(); + + return region; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehavior.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehavior.cs new file mode 100644 index 0000000000..9639f64dae --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehavior.cs @@ -0,0 +1,22 @@ +using System; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + public class MockRegionBehavior : IRegionBehavior + { + public IRegion Region + { + get; set; + } + + public Func OnAttach; + + public void Attach() + { + if (OnAttach != null) + OnAttach(); + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehaviorCollection.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehaviorCollection.cs new file mode 100644 index 0000000000..d6c2babfa3 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehaviorCollection.cs @@ -0,0 +1,14 @@ +using System.Collections; +using System.Collections.Generic; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockRegionBehaviorCollection : Dictionary, IRegionBehaviorCollection + { + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManager.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManager.cs new file mode 100644 index 0000000000..5d07db9f89 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManager.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Prism.Ioc; +using Prism.Navigation; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockRegionManager : IRegionManager + { + private IRegionCollection regions = new MockRegionCollection(); + internal MockRegionCollection MockRegionCollection + { + get + { + return regions as MockRegionCollection; + } + } + + public IRegionCollection Regions + { + get { return regions; } + } + + public IRegionManager CreateRegionManager() + { + throw new System.NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, object view) + { + throw new System.NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Type viewType) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public bool Navigate(System.Uri source) + { + throw new System.NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + } + + internal class MockRegionCollection : List, IRegionCollection + { + IEnumerator IEnumerable.GetEnumerator() + { + throw new System.NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IRegion this[string regionName] + { + get { return this[0]; } + } + + void IRegionCollection.Add(IRegion region) + { + Add(region); + } + + public bool Remove(string regionName) + { + throw new System.NotImplementedException(); + } + + public bool ContainsRegionWithName(string regionName) + { + return true; + } + + public void Add(string regionName, IRegion region) + { + throw new NotImplementedException(); + } + + public event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged; + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManagerAccessor.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManagerAccessor.cs new file mode 100644 index 0000000000..0a30efba37 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManagerAccessor.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + internal class MockRegionManagerAccessor : IRegionManagerAccessor + { + public Func GetRegionName; + public Func GetRegionManager; + + public event EventHandler UpdatingRegions; + + string IRegionManagerAccessor.GetRegionName(AvaloniaObject element) + { + return GetRegionName(element); + } + + IRegionManager IRegionManagerAccessor.GetRegionManager(AvaloniaObject element) + { + if (GetRegionManager != null) + return GetRegionManager(element); + + return null; + } + + public void UpdateRegions() + { + if (UpdatingRegions != null) + UpdatingRegions(this, EventArgs.Empty); + } + + public int GetSubscribersCount() + { + return UpdatingRegions != null ? UpdatingRegions.GetInvocationList().Length : 0; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockSortableViews.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockSortableViews.cs new file mode 100644 index 0000000000..996daa4a88 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockSortableViews.cs @@ -0,0 +1,19 @@ +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + [ViewSortHint("01")] + internal class MockSortableView1 + { + } + + [ViewSortHint("02")] + internal class MockSortableView2 + { + } + + [ViewSortHint("03")] + internal class MockSortableView3 + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockViewsCollection.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockViewsCollection.cs new file mode 100644 index 0000000000..173968c97d --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockViewsCollection.cs @@ -0,0 +1,39 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests.Mocks +{ + class MockViewsCollection : IViewsCollection + { + public ObservableCollection Items = new ObservableCollection(); + + public void Add(object view) + { + Items.Add(view); + } + + public bool Contains(object value) + { + return Items.Contains(value); + } + + public IEnumerator GetEnumerator() + { + return Items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add { Items.CollectionChanged += value; } + remove { Items.CollectionChanged -= value; } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAbstractModule.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAbstractModule.cs new file mode 100644 index 0000000000..bc27b94204 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAbstractModule.cs @@ -0,0 +1,23 @@ +using Prism.Ioc; +using Prism.Modularity; +using System; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public abstract class MockAbstractModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + + public class MockInheritingModule : MockAbstractModule + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAttributedModule.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAttributedModule.cs new file mode 100644 index 0000000000..ab18f82394 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAttributedModule.cs @@ -0,0 +1,20 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + [Module(ModuleName = "TestModule", OnDemand = true)] + public class MockAttributedModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependantModule.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependantModule.cs new file mode 100644 index 0000000000..ac16e3a5f7 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependantModule.cs @@ -0,0 +1,21 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + [Module(ModuleName = "DependantModule")] + [ModuleDependency("DependencyModule")] + public class DependantModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependencyModule.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependencyModule.cs new file mode 100644 index 0000000000..3c536acd53 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependencyModule.cs @@ -0,0 +1,20 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + [Module(ModuleName = "DependencyModule")] + public class DependencyModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockExposingTypeFromGacAssemblyModule.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockExposingTypeFromGacAssemblyModule.cs new file mode 100644 index 0000000000..ca9dda6325 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockExposingTypeFromGacAssemblyModule.cs @@ -0,0 +1,37 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public class MockExposingTypeFromGacAssemblyModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } + + public class SomeContractReferencingTransactionsAssembly : System.Transactions.IDtcTransaction + { + public void Commit(int retaining, int commitType, int reserved) + { + throw new System.NotImplementedException(); + } + + public void Abort(IntPtr reason, int retaining, int async) + { + throw new System.NotImplementedException(); + } + + public void GetTransactionInfo(IntPtr transactionInformation) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleA.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleA.cs new file mode 100644 index 0000000000..11ec670465 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleA.cs @@ -0,0 +1,23 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public class MockModuleA : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } + + public class DummyClass + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencedAssembly.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencedAssembly.cs new file mode 100644 index 0000000000..c58f26208a --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencedAssembly.cs @@ -0,0 +1,6 @@ +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public class MockReferencedModule + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingAssembly.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingAssembly.cs new file mode 100644 index 0000000000..9bead5751c --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingAssembly.cs @@ -0,0 +1,19 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public class MockModuleReferencingAssembly : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + MockReferencedModule instance = new MockReferencedModule(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingOtherModule.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingOtherModule.cs new file mode 100644 index 0000000000..5ef4553812 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingOtherModule.cs @@ -0,0 +1,22 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public class MockModuleReferencingOtherModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } + + public class MyDummyClass : DummyClass + { } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleThrowingException.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleThrowingException.cs new file mode 100644 index 0000000000..acc3695304 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleThrowingException.cs @@ -0,0 +1,19 @@ +using System; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Avalonia.Tests.Mocks.Modules +{ + public class MockModuleThrowingException : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockOptOutViewModel.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockOptOutViewModel.cs new file mode 100644 index 0000000000..56f09a372b --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockOptOutViewModel.cs @@ -0,0 +1,8 @@ +using Prism.Mvvm; + +namespace Prism.Avalonia.Tests.Mocks.ViewModels +{ + public class MockOptOutViewModel : BindableBase + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockViewModel.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockViewModel.cs new file mode 100644 index 0000000000..5c0b5655be --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockViewModel.cs @@ -0,0 +1,27 @@ +using Prism.Mvvm; + +namespace Prism.Avalonia.Tests.Mocks.ViewModels +{ + public class MockViewModel : BindableBase + { + private int mockProperty; + + public int MockProperty + { + get + { + return this.mockProperty; + } + + set + { + this.SetProperty(ref mockProperty, value); + } + } + + internal void InvokeOnPropertyChanged() + { + this.RaisePropertyChanged(nameof(MockProperty)); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/Mock.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/Mock.cs new file mode 100644 index 0000000000..2024251bf0 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/Mock.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace Prism.Avalonia.Tests.Mocks.Views +{ + public class Mock : Control + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockOptOut.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockOptOut.cs new file mode 100644 index 0000000000..e9167d6bdf --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockOptOut.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using Prism.Mvvm; + +namespace Prism.Avalonia.Tests.Mocks.Views +{ + public class MockOptOut : Control + { + public MockOptOut() + { + ViewModelLocator.SetAutoWireViewModel(this, false); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockView.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockView.cs new file mode 100644 index 0000000000..31c551f060 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockView.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace Prism.Avalonia.Tests.Mocks.Views +{ + public class MockView : Control + { + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/AssemblyResolverFixture.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/AssemblyResolverFixture.Desktop.cs new file mode 100644 index 0000000000..e103d31eb3 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/AssemblyResolverFixture.Desktop.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class AssemblyResolverFixture : IDisposable + { + private const string ModulesDirectory1 = @".\DynamicModules\MocksModulesAssemblyResolve"; + + public AssemblyResolverFixture() + { + CleanUpDirectories(); + } + + private void CleanUpDirectories() + { + CompilerHelper.CleanUpDirectory(ModulesDirectory1); + } + + [Fact] + public void ShouldThrowOnInvalidAssemblyFilePath() + { + bool exceptionThrown = false; + + using var resolver = new AssemblyResolver(); + + try + { + resolver.LoadAssemblyFrom(null); + } + catch (ArgumentException) + { + exceptionThrown = true; + } + + Assert.True(exceptionThrown); + + try + { + resolver.LoadAssemblyFrom("file://InexistentFile.dll"); + exceptionThrown = false; + } + catch (FileNotFoundException) + { + exceptionThrown = true; + } + + Assert.True(exceptionThrown); + + try + { + resolver.LoadAssemblyFrom("InvalidUri.dll"); + exceptionThrown = false; + } + catch (ArgumentException) + { + exceptionThrown = true; + } + + Assert.True(exceptionThrown); + } + + [Fact(Skip = "Operation is not supported on this platform")] + public void ShouldResolveTypeFromAbsoluteUriToAssembly() + { + string assemblyPath = CompilerHelper.GenerateDynamicModule("ModuleInLoadedFromContext1", "Module", ModulesDirectory1 + @"\ModuleInLoadedFromContext1.dll"); + var uriBuilder = new UriBuilder + { + Host = String.Empty, + Scheme = Uri.UriSchemeFile, + Path = Path.GetFullPath(assemblyPath) + }; + var assemblyUri = uriBuilder.Uri; + + using var resolver = new AssemblyResolver(); + + Type resolvedType = + Type.GetType( + "TestModules.ModuleInLoadedFromContext1Class, ModuleInLoadedFromContext1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + Assert.Null(resolvedType); + + resolver.LoadAssemblyFrom(assemblyUri.ToString()); + + resolvedType = + Type.GetType( + "TestModules.ModuleInLoadedFromContext1Class, ModuleInLoadedFromContext1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + Assert.NotNull(resolvedType); + } + + [Fact(Skip = "Operation is not supported on this platform")] + public void ShouldResolvePartialAssemblyName() + { + string assemblyPath = CompilerHelper.GenerateDynamicModule("ModuleInLoadedFromContext2", "Module", ModulesDirectory1 + @"\ModuleInLoadedFromContext2.dll"); + var uriBuilder = new UriBuilder + { + Host = String.Empty, + Scheme = Uri.UriSchemeFile, + Path = Path.GetFullPath(assemblyPath) + }; + var assemblyUri = uriBuilder.Uri; + + using var resolver = new AssemblyResolver(); + + resolver.LoadAssemblyFrom(assemblyUri.ToString()); + + Type resolvedType = + Type.GetType("TestModules.ModuleInLoadedFromContext2Class, ModuleInLoadedFromContext2"); + + Assert.NotNull(resolvedType); + + resolvedType = + Type.GetType("TestModules.ModuleInLoadedFromContext2Class, ModuleInLoadedFromContext2, Version=0.0.0.0"); + + Assert.NotNull(resolvedType); + + resolvedType = + Type.GetType("TestModules.ModuleInLoadedFromContext2Class, ModuleInLoadedFromContext2, Version=0.0.0.0, Culture=neutral"); + + Assert.NotNull(resolvedType); + } + + public void Dispose() + { + CleanUpDirectories(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationModuleCatalogFixture.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationModuleCatalogFixture.Desktop.cs new file mode 100644 index 0000000000..31552e5fea --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationModuleCatalogFixture.Desktop.cs @@ -0,0 +1,162 @@ +using System; +using System.Configuration; +using System.Linq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ConfigurationModuleCatalogFixture + { + [Fact] + public void CanInitConfigModuleEnumerator() + { + MockConfigurationStore store = new MockConfigurationStore(); + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog + { + Store = store + }; + Assert.NotNull(catalog); + } + + [Fact] + public void NullConfigurationStoreThrows() + { + var ex = Assert.Throws(() => + { + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = null }; + catalog.Load(); + }); + + } + + [Fact] + public void ShouldReturnAListOfModuleInfo() + { + MockConfigurationStore store = new MockConfigurationStore + { + Modules = new[] { new ModuleConfigurationElement(@"MocksModules\MockModuleA.dll", "TestModules.MockModuleAClass", "MockModuleA", false) } + }; + + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + + var modules = catalog.Modules; + + Assert.NotNull(modules); + Assert.Single(modules); + Assert.NotEqual(InitializationMode.WhenAvailable, modules.First().InitializationMode); + Assert.NotNull(modules.First().Ref); + Assert.StartsWith("file://", modules.First().Ref); + Assert.Contains(@"MocksModules/MockModuleA.dll", modules.First().Ref); + Assert.NotNull(modules.First().ModuleType); + Assert.Equal("TestModules.MockModuleAClass", modules.First().ModuleType); + + } + + [Fact] + public void GetZeroModules() + { + MockConfigurationStore store = new MockConfigurationStore(); + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + + Assert.Empty(catalog.Modules); + } + + [Fact] + public void EnumeratesThreeModulesWithDependencies() + { + var store = new MockConfigurationStore(); + var module1 = new ModuleConfigurationElement("Module1.dll", "Test.Module1", "Module1", false) + { + Dependencies = new ModuleDependencyCollection( + new[] { new ModuleDependencyConfigurationElement("Module2") }) + }; + + var module2 = new ModuleConfigurationElement("Module2.dll", "Test.Module2", "Module2", false) + { + Dependencies = new ModuleDependencyCollection( + new[] { new ModuleDependencyConfigurationElement("Module3") }) + }; + + var module3 = new ModuleConfigurationElement("Module3.dll", "Test.Module3", "Module3", false); + store.Modules = new[] { module3, module2, module1 }; + + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + + var modules = catalog.Modules; + + Assert.Equal(3, modules.Count()); + Assert.Contains(modules, module => module.ModuleName == "Module1"); + Assert.Contains(modules, module => module.ModuleName == "Module2"); + Assert.Contains(modules, module => module.ModuleName == "Module3"); + } + + [Fact] + public void EnumerateThrowsIfDuplicateNames() + { + var ex = Assert.Throws(() => + { + MockConfigurationStore store = new MockConfigurationStore(); + var module1 = new ModuleConfigurationElement("Module1.dll", "Test.Module1", "Module1", false); + var module2 = new ModuleConfigurationElement("Module2.dll", "Test.Module2", "Module1", false); + store.Modules = new[] { module2, module1 }; + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + }); + + } + + [Fact] + public void EnumerateNotThrowsIfDuplicateAssemblyFile() + { + MockConfigurationStore store = new MockConfigurationStore(); + var module1 = new ModuleConfigurationElement("Module1.dll", "Test.Module1", "Module1", false); + var module2 = new ModuleConfigurationElement("Module1.dll", "Test.Module2", "Module2", false); + store.Modules = new[] { module2, module1 }; + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + + Assert.Equal(2, catalog.Modules.Count()); + } + + [Fact] + public void GetStartupLoadedModulesDoesntRetrieveOnDemandLoaded() + { + MockConfigurationStore store = new MockConfigurationStore(); + var module1 = new ModuleConfigurationElement("Module1.dll", "Test.Module1", "Module1", false); + store.Modules = new[] { module1 }; + + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + + Assert.Single(catalog.Modules); + Assert.Equal(0, catalog.Modules.Count(m => m.InitializationMode != InitializationMode.OnDemand)); + } + + [Fact] + public void GetModulesNotThrownIfModuleSectionIsNotDeclared() + { + MockNullConfigurationStore store = new MockNullConfigurationStore(); + + ConfigurationModuleCatalog catalog = new ConfigurationModuleCatalog() { Store = store }; + catalog.Load(); + + var modules = catalog.Modules; + + Assert.NotNull(modules); + Assert.Empty(modules); + } + + internal class MockNullConfigurationStore : IConfigurationStore + { + public ModulesConfigurationSection RetrieveModuleConfigurationSection() + { + return null; + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationStoreFixture.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationStoreFixture.Desktop.cs new file mode 100644 index 0000000000..637127eaa9 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationStoreFixture.Desktop.cs @@ -0,0 +1,26 @@ +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ConfigurationStoreFixture + { + [Fact(Skip = "Needs upgraded to Avalonia")] + public void ShouldRetrieveModuleConfiguration() + { + ConfigurationStore store = new ConfigurationStore(); + var section = store.RetrieveModuleConfigurationSection(); + + Assert.NotNull(section); + Assert.NotNull(section.Modules); + Assert.Single(section.Modules); + Assert.NotNull(section.Modules[0].AssemblyFile); + Assert.Equal("MockModuleA", section.Modules[0].ModuleName); + Assert.NotNull(section.Modules[0].AssemblyFile); + Assert.Contains(@"MocksModules\MockModuleA.dll", section.Modules[0].AssemblyFile); + Assert.NotNull(section.Modules[0].ModuleType); + Assert.True(section.Modules[0].StartupLoaded); + Assert.Equal("Prism.Avalonia.Tests.Mocks.Modules.MockModuleA", section.Modules[0].ModuleType); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/DirectoryModuleCatalogFixture.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/DirectoryModuleCatalogFixture.Desktop.cs new file mode 100644 index 0000000000..8db56a2dc7 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/DirectoryModuleCatalogFixture.Desktop.cs @@ -0,0 +1,563 @@ +/* +#if DEBUG + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Security.Policy; +using System.Threading; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class DirectoryModuleCatalogFixture : IDisposable + { + private const string ModulesDirectory1 = @".\DynamicModules\MocksModules1"; + private const string ModulesDirectory2 = @".\DynamicModules\AttributedModules"; + private const string ModulesDirectory3 = @".\DynamicModules\DependantModules"; + private const string ModulesDirectory4 = @".\DynamicModules\MocksModules2"; + private const string ModulesDirectory5 = @".\DynamicModules\ModulesMainDomain\"; + private const string ModulesDirectory6 = @".\DynamicModules\Special char #"; + private const string InvalidModulesDirectory = @".\Modularity"; + + public DirectoryModuleCatalogFixture() + { + CleanUpDirectories(); + } + + private void CleanUpDirectories() + { + CompilerHelper.CleanUpDirectory(ModulesDirectory1); + CompilerHelper.CleanUpDirectory(ModulesDirectory2); + CompilerHelper.CleanUpDirectory(ModulesDirectory3); + CompilerHelper.CleanUpDirectory(ModulesDirectory4); + CompilerHelper.CleanUpDirectory(ModulesDirectory5); + CompilerHelper.CleanUpDirectory(InvalidModulesDirectory); + } + + [Fact] + public void NullPathThrows() + { + var ex = Assert.Throws(() => + { + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog(); + catalog.Load(); + }); + } + + [Fact] + public void EmptyPathThrows() + { + var ex = Assert.Throws(() => + { + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = string.Empty + }; + catalog.Load(); + }); + + } + + [Fact] + public void NonExistentPathThrows() + { + var ex = Assert.Throws(() => + { + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = "NonExistentPath" + }; + catalog.Load(); + }); + } + + [Fact] + public void ShouldReturnAListOfModuleInfo() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory1 + @"\MockModuleA.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory1 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.NotNull(modules); + Assert.Single(modules); + Assert.NotNull(modules[0].Ref); + Assert.StartsWith("file://", modules[0].Ref); + Assert.Contains(@"MockModuleA.dll", modules[0].Ref); + Assert.NotNull(modules[0].ModuleType); + Assert.Contains("Prism.Avalonia.Tests.Mocks.Modules.MockModuleA", modules[0].ModuleType); + } + + [Fact] + public void ShouldCorrectlyEscapeRef() + { + string assemblyPath = ModulesDirectory6 + @"\Mock Module #.dll"; + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", assemblyPath); + string fullAssemblyPath = Path.GetFullPath(assemblyPath); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory6 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.NotNull(modules); + Assert.Single(modules); + Assert.NotNull(modules[0].Ref); + + string moduleRef = modules[0].Ref; + // = new Uri(moduleRef); + Assert.True(Uri.TryCreate(moduleRef, UriKind.Absolute, out Uri moduleUri)); + + Assert.Equal(fullAssemblyPath, moduleUri.LocalPath); + } + + //TODO: figure out how ot translat ehtese tests to Xunit + //[Fact] + //[DeploymentItem(@"Modularity\NotAValidDotNetDll.txt.dll", @".\Modularity")] + //public void ShouldNotThrowWithNonValidDotNetAssembly() + //{ + // DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + // { + // ModulePath = InvalidModulesDirectory + // }; + // try + // { + // catalog.Load(); + // } + // catch (Exception) + // { + // //Assert.Fail("Should not have thrown."); + // } + + // var modules = catalog.Modules.ToArray(); + // Assert.NotNull(modules); + // Assert.Equal(0, modules.Length); + //} + + //[Fact] + //[DeploymentItem(@"Modularity\NotAValidDotNetDll.txt.dll", InvalidModulesDirectory)] + //public void LoadsValidAssembliesWhenInvalidDllsArePresent() + //{ + // CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + // InvalidModulesDirectory + @"\MockModuleA.dll"); + + // DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + // { + // ModulePath = InvalidModulesDirectory + // }; + // try + // { + // catalog.Load(); + // } + // catch (Exception) + // { + // //Assert.Fail("Should not have thrown."); + // } + + // var modules = catalog.Modules.ToArray(); + + // Assert.NotNull(modules); + // Assert.Equal(1, modules.Length); + // Assert.NotNull(modules[0].Ref); + // Assert.StartsWith(modules[0].Ref, "file://"); + // Assert.True(modules[0].Ref.Contains(@"MockModuleA.dll")); + // Assert.NotNull(modules[0].ModuleType); + // Assert.Contains(modules[0].ModuleType, "Prism.Avalonia.Tests.Mocks.Modules.MockModuleA"); + //} + + [Fact] + public void ShouldNotThrowWithLoadFromByteAssemblies() + { + CompilerHelper.CleanUpDirectory(@".\CompileOutput\"); + CompilerHelper.CleanUpDirectory(@".\IgnoreLoadFromByteAssembliesTestDir\"); + var results = CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + @".\CompileOutput\MockModuleA.dll"); + + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockAttributedModule.cs", + @".\IgnoreLoadFromByteAssembliesTestDir\MockAttributedModule.dll"); + + string path = @".\IgnoreLoadFromByteAssembliesTestDir"; + + AppDomain testDomain = null; + try + { + testDomain = CreateAppDomain(); + RemoteDirectoryLookupCatalog remoteEnum = CreateRemoteDirectoryModuleCatalogInAppDomain(testDomain); + + remoteEnum.LoadDynamicEmittedModule(); + + remoteEnum.LoadAssembliesByByte(@".\CompileOutput\MockModuleA.dll"); + + var infos = remoteEnum.DoEnumeration(path); + + Assert.NotNull( + infos.FirstOrDefault(x => x.ModuleType.IndexOf("Prism.Avalonia.Tests.Mocks.Modules.MockAttributedModule") >= 0) + ); + } + finally + { + if (testDomain != null) + AppDomain.Unload(testDomain); + } + } + + [Fact] + public void ShouldGetModuleNameFromAttribute() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockAttributedModule.cs", + ModulesDirectory2 + @"\MockAttributedModule.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory2 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.Single(modules); + Assert.Equal("TestModule", modules[0].ModuleName); + } + + [Fact] + public void ShouldGetDependantModulesFromAttribute() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockDependencyModule.cs", + ModulesDirectory3 + @"\DependencyModule.dll"); + + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockDependantModule.cs", + ModulesDirectory3 + @"\DependantModule.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory3 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.Equal(2, modules.Length); + var dependantModule = modules.First(module => module.ModuleName == "DependantModule"); + var dependencyModule = modules.First(module => module.ModuleName == "DependencyModule"); + Assert.NotNull(dependantModule); + Assert.NotNull(dependencyModule); + Assert.NotNull(dependantModule.DependsOn); + Assert.Single(dependantModule.DependsOn); + Assert.Equal(dependencyModule.ModuleName, dependantModule.DependsOn[0]); + } + + [Fact] + public void UseClassNameAsModuleNameWhenNotSpecifiedInAttribute() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory1 + @"\MockModuleA.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory1 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.NotNull(modules); + Assert.Equal("MockModuleA", modules[0].ModuleName); + } + + [Fact] + public void ShouldDefaultInitializationModeToWhenAvailable() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory1 + @"\MockModuleA.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory1 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.NotNull(modules); + Assert.Equal(InitializationMode.WhenAvailable, modules[0].InitializationMode); + } + + [Fact] + public void ShouldGetOnDemandFromAttribute() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockAttributedModule.cs", + ModulesDirectory3 + @"\MockAttributedModule.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory3 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.Single(modules); + Assert.Equal(InitializationMode.OnDemand, modules[0].InitializationMode); + + } + + [Fact] + public void ShouldNotLoadAssembliesInCurrentAppDomain() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory4 + @"\MockModuleA.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory4 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + // filtering out dynamic assemblies due to using a dynamic mocking framework. + Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().Where(assembly => !assembly.IsDynamic) + .Where(assembly => assembly.Location.Equals(modules[0].Ref, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); + Assert.Null(loadedAssembly); + } + + [Fact] + public void ShouldNotGetModuleInfoForAnAssemblyAlreadyLoadedInTheMainDomain() + { + var assemblyPath = Assembly.GetCallingAssembly().Location; + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory5 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.Empty(modules); + } + + [Fact] + public void ShouldLoadAssemblyEvenIfTheyAreReferencingEachOther() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory4 + @"\MockModuleZZZ.dll"); + + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleReferencingOtherModule.cs", + ModulesDirectory4 + @"\MockModuleReferencingOtherModule.dll", ModulesDirectory4 + @"\MockModuleZZZ.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory4 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.Equal(2, modules.Count()); + } + //Disabled Warning + // 'System.Security.Policy.Evidence.Count' is obsolete: ' + // "Evidence should not be treated as an ICollection. Please use GetHostEnumerator and GetAssemblyEnumerator to + // iterate over the evidence to collect a count."' +#pragma warning disable 0618 + [Fact] + public void CreateChildAppDomainHasParentEvidenceAndSetup() + { + TestableDirectoryModuleCatalog catalog = new TestableDirectoryModuleCatalog + { + ModulePath = ModulesDirectory4 + }; + catalog.Load(); + Evidence parentEvidence = new Evidence(); + AppDomainSetup parentSetup = new AppDomainSetup + { + ApplicationName = "Test Parent" + }; + AppDomain parentAppDomain = AppDomain.CreateDomain("Parent", parentEvidence, parentSetup); + AppDomain childDomain = catalog.BuildChildDomain(parentAppDomain); + + Assert.Equal(parentEvidence.Count, childDomain.Evidence.Count); + Assert.Equal("Test Parent", childDomain.SetupInformation.ApplicationName); + Assert.NotEqual(AppDomain.CurrentDomain.Evidence.Count, childDomain.Evidence.Count); + Assert.NotEqual(AppDomain.CurrentDomain.SetupInformation.ApplicationName, childDomain.SetupInformation.ApplicationName); + } +#pragma warning restore 0618 + + [Fact] + public void ShouldLoadFilesEvenIfDynamicAssemblyExists() + { + CompilerHelper.CleanUpDirectory(@".\CompileOutput\"); + CompilerHelper.CleanUpDirectory(@".\IgnoreDynamicGeneratedFilesTestDir\"); + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockAttributedModule.cs", + @".\IgnoreDynamicGeneratedFilesTestDir\MockAttributedModule.dll"); + + string path = @".\IgnoreDynamicGeneratedFilesTestDir"; + + AppDomain testDomain = null; + try + { + testDomain = CreateAppDomain(); + RemoteDirectoryLookupCatalog remoteEnum = CreateRemoteDirectoryModuleCatalogInAppDomain(testDomain); + + remoteEnum.LoadDynamicEmittedModule(); + + var infos = remoteEnum.DoEnumeration(path); + + Assert.NotNull( + infos.FirstOrDefault(x => x.ModuleType.IndexOf("Prism.Avalonia.Tests.Mocks.Modules.MockAttributedModule") >= 0) + ); + } + finally + { + if (testDomain != null) + AppDomain.Unload(testDomain); + } + } + + [Fact] + public void ShouldLoadAssemblyEvenIfIsExposingTypesFromAnAssemblyInTheGac() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockExposingTypeFromGacAssemblyModule.cs", + ModulesDirectory4 + @"\MockExposingTypeFromGacAssemblyModule.dll", @"System.Transactions.dll"); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory4 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + + Assert.Single(modules); + } + + [Fact] + public void ShouldNotFailWhenAlreadyLoadedAssembliesAreAlsoFoundOnTargetDirectory() + { + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory1 + @"\MockModuleA.dll"); + + string filename = typeof(DirectoryModuleCatalog).Assembly.Location; + string destinationFileName = Path.Combine(ModulesDirectory1, Path.GetFileName(filename)); + File.Copy(filename, destinationFileName); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory1 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + Assert.Single(modules); + } + + [Fact] + public void ShouldIgnoreAbstractClassesThatImplementIModule() + { + CompilerHelper.CleanUpDirectory(ModulesDirectory1); + CompilerHelper.CompileFile(@"Prism.Avalonia.Tests.Mocks.Modules.MockAbstractModule.cs", + ModulesDirectory1 + @"\MockAbstractModule.dll"); + + string filename = typeof(DirectoryModuleCatalog).Assembly.Location; + string destinationFileName = Path.Combine(ModulesDirectory1, Path.GetFileName(filename)); + File.Copy(filename, destinationFileName); + + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = ModulesDirectory1 + }; + catalog.Load(); + + var modules = catalog.Modules.ToArray(); + Assert.Single(modules); + Assert.Equal("MockInheritingModule", modules[0].ModuleName); + + CompilerHelper.CleanUpDirectory(ModulesDirectory1); + } + + private AppDomain CreateAppDomain() + { + Evidence evidence = AppDomain.CurrentDomain.Evidence; + AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation; + + return AppDomain.CreateDomain("TestDomain", evidence, setup); + } + + private RemoteDirectoryLookupCatalog CreateRemoteDirectoryModuleCatalogInAppDomain(AppDomain testDomain) + { + RemoteDirectoryLookupCatalog remoteEnum; + Type remoteEnumType = typeof(RemoteDirectoryLookupCatalog); + + remoteEnum = (RemoteDirectoryLookupCatalog)testDomain.CreateInstanceFrom( + remoteEnumType.Assembly.Location, remoteEnumType.FullName).Unwrap(); + return remoteEnum; + } + + public void Dispose() + { + CleanUpDirectories(); + } + + private class TestableDirectoryModuleCatalog : DirectoryModuleCatalog + { + public new AppDomain BuildChildDomain(AppDomain currentDomain) + { + return base.BuildChildDomain(currentDomain); + } + } + + private class RemoteDirectoryLookupCatalog : MarshalByRefObject + { + + public void LoadAssembliesByByte(string assemblyPath) + { + byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); + AppDomain.CurrentDomain.Load(assemblyBytes); + } + + public IModuleInfo[] DoEnumeration(string path) + { + DirectoryModuleCatalog catalog = new DirectoryModuleCatalog + { + ModulePath = path + }; + catalog.Load(); + return catalog.Modules.ToArray(); + } + + public void LoadDynamicEmittedModule() + { + // create a dynamic assembly and module + AssemblyName assemblyName = new AssemblyName + { + Name = "DynamicBuiltAssembly" + }; + AssemblyBuilder assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave); + ModuleBuilder module = assemblyBuilder.DefineDynamicModule("DynamicBuiltAssembly.dll"); + + // create a new type + TypeBuilder typeBuilder = module.DefineType("DynamicBuiltType", TypeAttributes.Public | TypeAttributes.Class); + + // Create the type + Type helloWorldType = typeBuilder.CreateType(); + + } + } + } +} +#endif +*/ diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/FileModuleTypeLoaderFixture.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/FileModuleTypeLoaderFixture.Desktop.cs new file mode 100644 index 0000000000..dfcaa41239 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/FileModuleTypeLoaderFixture.Desktop.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.ObjectModel; +using Moq; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class FileModuleTypeLoaderFixture + { + [Fact(Skip = "CompilerHelper.CompileCode needs updated")] + public void CanRetrieveModule() + { + var assemblyResolver = new MockAssemblyResolver(); + var retriever = new FileModuleTypeLoader(assemblyResolver); + string assembly = CompilerHelper.GenerateDynamicModule("FileModuleA", null); + string assemblyRef = "file://" + assembly; + var fileModuleInfo = CreateModuleInfo(assemblyRef, "TestModules.FileModuleAClass", "ModuleA", true, null); + + bool loadCompleted = false; + retriever.LoadModuleCompleted += delegate (object sender, LoadModuleCompletedEventArgs e) + { + loadCompleted = true; + }; + + retriever.LoadModuleType(fileModuleInfo); + + Assert.True(loadCompleted); + Assert.Equal(assemblyRef, assemblyResolver.LoadAssemblyFromArgument); + } + + [Fact] + public void ShouldReturnErrorToCallback() + { + var assemblyResolver = new MockAssemblyResolver(); + var retriever = new FileModuleTypeLoader(assemblyResolver); + var fileModuleInfo = CreateModuleInfo("NonExistentFile.dll", "NonExistentModule", "NonExistent", true, null); + + assemblyResolver.ThrowOnLoadAssemblyFrom = true; + Exception resultException = null; + + bool loadCompleted = false; + retriever.LoadModuleCompleted += delegate (object sender, LoadModuleCompletedEventArgs e) + { + loadCompleted = true; + resultException = e.Error; + }; + + retriever.LoadModuleType(fileModuleInfo); + + Assert.True(loadCompleted); + Assert.NotNull(resultException); + } + + [Fact] + public void CanRetrieveWithCorrectRef() + { + var retriever = new FileModuleTypeLoader(); + var moduleInfo = new ModuleInfo() { Ref = "file://somefile" }; + + Assert.True(retriever.CanLoadModuleType(moduleInfo)); + } + + [Fact] + public void CannotRetrieveWithIncorrectRef() + { + var retriever = new FileModuleTypeLoader(); + var moduleInfo = new ModuleInfo() { Ref = "NotForLocalRetrieval" }; + + Assert.False(retriever.CanLoadModuleType(moduleInfo)); + } + + [Fact] + public void FileModuleTypeLoaderCanBeDisposed() + { + var typeLoader = new FileModuleTypeLoader(); + var disposable = typeLoader as IDisposable; + + Assert.NotNull(disposable); + } + + [Fact] + public void FileModuleTypeLoaderDisposeNukesAssemblyResolver() + { + Mock mockResolver = new Mock(); + var disposableMockResolver = mockResolver.As(); + disposableMockResolver.Setup(resolver => resolver.Dispose()); + + var typeLoader = new FileModuleTypeLoader(mockResolver.Object); + + typeLoader.Dispose(); + + disposableMockResolver.Verify(resolver => resolver.Dispose(), Times.Once()); + } + + [Fact] + public void FileModuleTypeLoaderDisposeDoesNotThrowWithNonDisposableAssemblyResolver() + { + Mock mockResolver = new Mock(); + var typeLoader = new FileModuleTypeLoader(mockResolver.Object); + try + { + typeLoader.Dispose(); + } + catch (Exception) + { + //Assert.Fail(); + } + } + + private static ModuleInfo CreateModuleInfo(string assemblyFile, string moduleType, string moduleName, bool startupLoaded, params string[] dependsOn) + { + ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleType) + { + InitializationMode = startupLoaded ? InitializationMode.WhenAvailable : InitializationMode.OnDemand, + Ref = assemblyFile, + }; + if (dependsOn != null) + { + moduleInfo.DependsOn.AddRange(dependsOn); + } + + return moduleInfo; + } + } + + internal class MockAssemblyResolver : IAssemblyResolver + { + public string LoadAssemblyFromArgument; + public bool ThrowOnLoadAssemblyFrom; + + public void LoadAssemblyFrom(string assemblyFilePath) + { + LoadAssemblyFromArgument = assemblyFilePath; + if (ThrowOnLoadAssemblyFrom) + throw new Exception(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleAttributeFixture.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleAttributeFixture.Desktop.cs new file mode 100644 index 0000000000..fe0af2c001 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleAttributeFixture.Desktop.cs @@ -0,0 +1,35 @@ +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ModuleAttributeFixture + { + [Fact] + public void StartupLoadedDefaultsToTrue() + { + var moduleAttribute = new ModuleAttribute(); + + Assert.False(moduleAttribute.OnDemand); + } + + [Fact] + public void CanGetAndSetProperties() + { + var moduleAttribute = new ModuleAttribute(); + moduleAttribute.ModuleName = "Test"; + moduleAttribute.OnDemand = true; + + Assert.Equal("Test", moduleAttribute.ModuleName); + Assert.True(moduleAttribute.OnDemand); + } + + [Fact] + public void ModuleDependencyAttributeStoresModuleName() + { + var moduleDependencyAttribute = new ModuleDependencyAttribute("Test"); + + Assert.Equal("Test", moduleDependencyAttribute.ModuleName); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogFixture.cs new file mode 100644 index 0000000000..31486adf39 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogFixture.cs @@ -0,0 +1,481 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ModuleCatalogFixture + { + [Fact] + public void CanCreateCatalogFromList() + { + var moduleInfo = new ModuleInfo("MockModule", "type"); + List moduleInfos = new List { moduleInfo }; + + var moduleCatalog = new ModuleCatalog(moduleInfos); + + Assert.Single(moduleCatalog.Modules); + Assert.Equal(moduleInfo, moduleCatalog.Modules.ElementAt(0)); + } + + [Fact] + public void CanGetDependenciesForModule() + { + // A <- B + var moduleInfoA = CreateModuleInfo("A"); + var moduleInfoB = CreateModuleInfo("B", "A"); + List moduleInfos = new List + { + moduleInfoA + , moduleInfoB + }; + var moduleCatalog = new ModuleCatalog(moduleInfos); + + var dependentModules = moduleCatalog.GetDependentModules(moduleInfoB); + + Assert.Single(dependentModules); + Assert.Equal(moduleInfoA, dependentModules.ElementAt(0)); + } + + [Fact] + public void CanCompleteListWithTheirDependencies() + { + // A <- B <- C + var moduleInfoA = CreateModuleInfo("A"); + var moduleInfoB = CreateModuleInfo("B", "A"); + var moduleInfoC = CreateModuleInfo("C", "B"); + var moduleInfoOrphan = CreateModuleInfo("X", "B"); + + List moduleInfos = new List + { + moduleInfoA + , moduleInfoB + , moduleInfoC + , moduleInfoOrphan + }; + var moduleCatalog = new ModuleCatalog(moduleInfos); + + var dependantModules = moduleCatalog.CompleteListWithDependencies(new[] { moduleInfoC }); + + Assert.Equal(3, dependantModules.Count()); + Assert.Contains(moduleInfoA, dependantModules); + Assert.Contains(moduleInfoB, dependantModules); + Assert.Contains(moduleInfoC, dependantModules); + } + + [Fact] + public void ShouldThrowOnCyclicDependency() + { + var ex = Assert.Throws(() => + { + // A <- B <- C <- A + var moduleInfoA = CreateModuleInfo("A", "C"); + var moduleInfoB = CreateModuleInfo("B", "A"); + var moduleInfoC = CreateModuleInfo("C", "B"); + + List moduleInfos = new List + { + moduleInfoA, + moduleInfoB, + moduleInfoC, + }; + new ModuleCatalog(moduleInfos).Validate(); + }); + + } + + [Fact] + public void ShouldThrowOnDuplicateModule() + { + var ex = Assert.Throws(() => + { + var moduleInfoA1 = CreateModuleInfo("A"); + var moduleInfoA2 = CreateModuleInfo("A"); + + List moduleInfos = new List + { + moduleInfoA1, + moduleInfoA2, + }; + new ModuleCatalog(moduleInfos).Validate(); + }); + } + + [Fact] + public void ShouldThrowOnMissingDependency() + { + var ex = Assert.Throws(() => + { + var moduleInfoA = CreateModuleInfo("A", "B"); + + List moduleInfos = new List + { + moduleInfoA, + }; + new ModuleCatalog(moduleInfos).Validate(); + }); + } + + [Fact] + public void CanAddModules() + { + var catalog = new ModuleCatalog(); + + catalog.AddModule(typeof(MockModule)); + + Assert.Single(catalog.Modules); + Assert.Equal("MockModule", catalog.Modules.First().ModuleName); + } + + [Fact] + public void CanAddGroups() + { + var catalog = new ModuleCatalog(); + + ModuleInfo moduleInfo = new ModuleInfo(); + ModuleInfoGroup group = new ModuleInfoGroup { moduleInfo }; + catalog.Items.Add(group); + + Assert.Single(catalog.Modules); + Assert.Same(moduleInfo, catalog.Modules.ElementAt(0)); + } + + [Fact] + public void ShouldAggregateGroupsAndLooseModuleInfos() + { + var catalog = new ModuleCatalog(); + ModuleInfo moduleInfo1 = new ModuleInfo(); + ModuleInfo moduleInfo2 = new ModuleInfo(); + ModuleInfo moduleInfo3 = new ModuleInfo(); + + catalog.Items.Add(new ModuleInfoGroup() { moduleInfo1 }); + catalog.Items.Add(new ModuleInfoGroup() { moduleInfo2 }); + catalog.AddModule(moduleInfo3); + + Assert.Equal(3, catalog.Modules.Count()); + Assert.Contains(moduleInfo1, catalog.Modules); + Assert.Contains(moduleInfo2, catalog.Modules); + Assert.Contains(moduleInfo3, catalog.Modules); + } + + [Fact] + public void CompleteListWithDependenciesThrowsWithNull() + { + var ex = Assert.Throws(() => + { + var catalog = new ModuleCatalog(); + catalog.CompleteListWithDependencies(null); + }); + + } + + [Fact] + public void LooseModuleIfDependentOnModuleInGroupThrows() + { + var catalog = new ModuleCatalog(); + catalog.Items.Add(new ModuleInfoGroup() { CreateModuleInfo("ModuleA") }); + catalog.AddModule(CreateModuleInfo("ModuleB", "ModuleA")); + + try + { + catalog.Validate(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Equal("ModuleB", ((ModularityException)ex).ModuleName); + + return; + } + + //Assert.Fail("Exception not thrown."); + } + + [Fact] + public void ModuleInGroupDependsOnModuleInOtherGroupThrows() + { + var catalog = new ModuleCatalog(); + catalog.Items.Add(new ModuleInfoGroup() { CreateModuleInfo("ModuleA") }); + catalog.Items.Add(new ModuleInfoGroup() { CreateModuleInfo("ModuleB", "ModuleA") }); + + try + { + catalog.Validate(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Equal("ModuleB", ((ModularityException)ex).ModuleName); + + return; + } + + //Assert.Fail("Exception not thrown."); + } + + [Fact] + public void ShouldRevalidateWhenAddingNewModuleIfValidated() + { + var testableCatalog = new TestableModuleCatalog(); + testableCatalog.Items.Add(new ModuleInfoGroup() { CreateModuleInfo("ModuleA") }); + testableCatalog.Validate(); + testableCatalog.Items.Add(new ModuleInfoGroup() { CreateModuleInfo("ModuleB") }); + Assert.True(testableCatalog.ValidateCalled); + } + + [Fact] + public void ModuleInGroupCanDependOnModuleInSameGroup() + { + var catalog = new ModuleCatalog(); + var moduleA = CreateModuleInfo("ModuleA"); + var moduleB = CreateModuleInfo("ModuleB", "ModuleA"); + catalog.Items.Add(new ModuleInfoGroup() + { + moduleA, + moduleB, + }); + + var moduleBDependencies = catalog.GetDependentModules(moduleB); + + Assert.Single(moduleBDependencies); + Assert.Equal(moduleA, moduleBDependencies.First()); + + } + + [Fact] + public void StartupModuleDependentOnAnOnDemandModuleThrows() + { + var catalog = new ModuleCatalog(); + var moduleOnDemand = CreateModuleInfo("ModuleA"); + moduleOnDemand.InitializationMode = InitializationMode.OnDemand; + catalog.AddModule(moduleOnDemand); + catalog.AddModule(CreateModuleInfo("ModuleB", "ModuleA")); + + try + { + catalog.Validate(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Equal("ModuleB", ((ModularityException)ex).ModuleName); + + return; + } + + //Assert.Fail("Exception not thrown."); + } + + [Fact] + public void ShouldReturnInCorrectRetrieveOrderWhenCompletingListWithDependencies() + { + // A <- B <- C <- D, C <- X + var moduleA = CreateModuleInfo("A"); + var moduleB = CreateModuleInfo("B", "A"); + var moduleC = CreateModuleInfo("C", "B"); + var moduleD = CreateModuleInfo("D", "C"); + var moduleX = CreateModuleInfo("X", "C"); + + var moduleCatalog = new ModuleCatalog(); + // Add the modules in random order + moduleCatalog.AddModule(moduleB); + moduleCatalog.AddModule(moduleA); + moduleCatalog.AddModule(moduleD); + moduleCatalog.AddModule(moduleX); + moduleCatalog.AddModule(moduleC); + + var dependantModules = moduleCatalog.CompleteListWithDependencies(new[] { moduleD, moduleX }).ToList(); + + Assert.Equal(5, dependantModules.Count); + Assert.True(dependantModules.IndexOf(moduleA) < dependantModules.IndexOf(moduleB)); + Assert.True(dependantModules.IndexOf(moduleB) < dependantModules.IndexOf(moduleC)); + Assert.True(dependantModules.IndexOf(moduleC) < dependantModules.IndexOf(moduleD)); + Assert.True(dependantModules.IndexOf(moduleC) < dependantModules.IndexOf(moduleX)); + } + + [Fact] + public void ShouldLoadAndValidateOnInitialize() + { + var catalog = new TestableModuleCatalog(); + + var testableCatalog = new TestableModuleCatalog(); + Assert.False(testableCatalog.LoadCalled); + Assert.False(testableCatalog.ValidateCalled); + + testableCatalog.Initialize(); + Assert.True(testableCatalog.LoadCalled); + Assert.True(testableCatalog.ValidateCalled); + Assert.True(testableCatalog.LoadCalledFirst); + } + + [Fact] + public void ShouldNotLoadAgainIfInitializedCalledMoreThanOnce() + { + var catalog = new TestableModuleCatalog(); + + var testableCatalog = new TestableModuleCatalog(); + Assert.False(testableCatalog.LoadCalled); + Assert.False(testableCatalog.ValidateCalled); + + testableCatalog.Initialize(); + Assert.Equal(1, testableCatalog.LoadCalledCount); + testableCatalog.Initialize(); + Assert.Equal(1, testableCatalog.LoadCalledCount); + } + + [Fact] + public void ShouldNotLoadAgainDuringInitialize() + { + var catalog = new TestableModuleCatalog(); + + var testableCatalog = new TestableModuleCatalog(); + Assert.False(testableCatalog.LoadCalled); + Assert.False(testableCatalog.ValidateCalled); + + testableCatalog.Load(); + Assert.Equal(1, testableCatalog.LoadCalledCount); + testableCatalog.Initialize(); + Assert.Equal(1, testableCatalog.LoadCalledCount); + } + + [Fact] + public void ShouldAllowLoadToBeInvokedTwice() + { + var catalog = new TestableModuleCatalog(); + + var testableCatalog = new TestableModuleCatalog(); + testableCatalog.Load(); + Assert.Equal(1, testableCatalog.LoadCalledCount); + testableCatalog.Load(); + Assert.Equal(2, testableCatalog.LoadCalledCount); + } + + [Fact] + public void CanAddModule1() + { + ModuleCatalog catalog = new ModuleCatalog(); + + catalog.AddModule("Module", "ModuleType", InitializationMode.OnDemand, "DependsOn1", "DependsOn2"); + + Assert.Single(catalog.Modules); + Assert.Equal("Module", catalog.Modules.First().ModuleName); + Assert.Equal("ModuleType", catalog.Modules.First().ModuleType); + Assert.Equal(InitializationMode.OnDemand, catalog.Modules.First().InitializationMode); + Assert.Equal(2, catalog.Modules.First().DependsOn.Count); + Assert.Equal("DependsOn1", catalog.Modules.First().DependsOn[0]); + Assert.Equal("DependsOn2", catalog.Modules.First().DependsOn[1]); + } + + [Fact] + public void CanAddModule2() + { + ModuleCatalog catalog = new ModuleCatalog(); + + catalog.AddModule("Module", "ModuleType", "DependsOn1", "DependsOn2"); + + Assert.Single(catalog.Modules); + Assert.Equal("Module", catalog.Modules.First().ModuleName); + Assert.Equal("ModuleType", catalog.Modules.First().ModuleType); + Assert.Equal(InitializationMode.WhenAvailable, catalog.Modules.First().InitializationMode); + Assert.Equal(2, catalog.Modules.First().DependsOn.Count); + Assert.Equal("DependsOn1", catalog.Modules.First().DependsOn[0]); + Assert.Equal("DependsOn2", catalog.Modules.First().DependsOn[1]); + + } + [Fact] + public void CanAddModule3() + { + ModuleCatalog catalog = new ModuleCatalog(); + + catalog.AddModule(typeof(MockModule), InitializationMode.OnDemand, "DependsOn1", "DependsOn2"); + + Assert.Single(catalog.Modules); + Assert.Equal("MockModule", catalog.Modules.First().ModuleName); + Assert.Equal(typeof(MockModule).AssemblyQualifiedName, catalog.Modules.First().ModuleType); + Assert.Equal(InitializationMode.OnDemand, catalog.Modules.First().InitializationMode); + Assert.Equal(2, catalog.Modules.First().DependsOn.Count); + Assert.Equal("DependsOn1", catalog.Modules.First().DependsOn[0]); + Assert.Equal("DependsOn2", catalog.Modules.First().DependsOn[1]); + + } + + [Fact] + public void CanAddModule4() + { + ModuleCatalog catalog = new ModuleCatalog(); + + catalog.AddModule(typeof(MockModule), "DependsOn1", "DependsOn2"); + + Assert.Single(catalog.Modules); + Assert.Equal("MockModule", catalog.Modules.First().ModuleName); + Assert.Equal(typeof(MockModule).AssemblyQualifiedName, catalog.Modules.First().ModuleType); + Assert.Equal(InitializationMode.WhenAvailable, catalog.Modules.First().InitializationMode); + Assert.Equal(2, catalog.Modules.First().DependsOn.Count); + Assert.Equal("DependsOn1", catalog.Modules.First().DependsOn[0]); + Assert.Equal("DependsOn2", catalog.Modules.First().DependsOn[1]); + + } + + [Fact] + public void CanAddGroup() + { + ModuleCatalog catalog = new ModuleCatalog(); + + catalog.Items.Add(new ModuleInfoGroup()); + + catalog.AddGroup(InitializationMode.OnDemand, "Ref1", + new ModuleInfo("M1", "T1"), + new ModuleInfo("M2", "T2", "M1")); + + Assert.Equal(2, catalog.Modules.Count()); + + var module1 = catalog.Modules.First(); + var module2 = catalog.Modules.Skip(1).First(); + + Assert.Equal("M1", module1.ModuleName); + Assert.Equal("T1", module1.ModuleType); + Assert.Equal("Ref1", module1.Ref); + Assert.Equal(InitializationMode.OnDemand, module1.InitializationMode); + + Assert.Equal("M2", module2.ModuleName); + Assert.Equal("T2", module2.ModuleType); + Assert.Equal("Ref1", module2.Ref); + Assert.Equal(InitializationMode.OnDemand, module2.InitializationMode); + } + + private class TestableModuleCatalog : ModuleCatalog + { + public bool ValidateCalled { get; set; } + public bool LoadCalledFirst { get; set; } + public bool LoadCalled + { + get { return LoadCalledCount > 0; } + } + public int LoadCalledCount { get; set; } + + public override void Validate() + { + ValidateCalled = true; + Validated = true; + } + + protected override void InnerLoad() + { + if (ValidateCalled == false && !LoadCalled) + LoadCalledFirst = true; + + LoadCalledCount++; + } + } + + private static ModuleInfo CreateModuleInfo(string name, params string[] dependsOn) + { + ModuleInfo moduleInfo = new ModuleInfo(name, name); + moduleInfo.DependsOn.AddRange(dependsOn); + return moduleInfo; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/InvalidDependencyModuleCatalog.xaml b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/InvalidDependencyModuleCatalog.xaml new file mode 100644 index 0000000000..65ac908944 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/InvalidDependencyModuleCatalog.xaml @@ -0,0 +1,18 @@ + + + + + + + InvalidModuleDependency + + + + + + + + diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/SimpleModuleCatalog.xaml b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/SimpleModuleCatalog.xaml new file mode 100644 index 0000000000..4046e68a19 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/SimpleModuleCatalog.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + Module3InModuleGroup2 + + + + + + + + + + + + + + diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleDependencySolverFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleDependencySolverFixture.cs new file mode 100644 index 0000000000..5c8e2c359c --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleDependencySolverFixture.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ModuleDependencySolverFixture + { + private ModuleDependencySolver solver; + + public ModuleDependencySolverFixture() + { + solver = new ModuleDependencySolver(); + } + + [Fact] + public void ModuleDependencySolverIsAvailable() + { + Assert.NotNull(solver); + } + + [Fact] + public void CanAddModuleName() + { + solver.AddModule("ModuleA"); + Assert.Equal(1, solver.ModuleCount); + } + + [Fact] + public void CannotAddNullModuleName() + { + var ex = Assert.Throws(() => + { + solver.AddModule(null); + }); + + } + + [Fact] + public void CannotAddEmptyModuleName() + { + var ex = Assert.Throws(() => + { + solver.AddModule(String.Empty); + }); + + } + + [Fact] + public void CannotAddDependencyWithoutAddingModule() + { + var ex = Assert.Throws(() => + { + solver.AddDependency("ModuleA", "ModuleB"); + }); + + } + + [Fact] + public void CanAddModuleDepedency() + { + solver.AddModule("ModuleA"); + solver.AddModule("ModuleB"); + solver.AddDependency("ModuleB", "ModuleA"); + Assert.Equal(2, solver.ModuleCount); + } + + [Fact] + public void CanSolveAcyclicDependencies() + { + solver.AddModule("ModuleA"); + solver.AddModule("ModuleB"); + solver.AddDependency("ModuleB", "ModuleA"); + string[] result = solver.Solve(); + Assert.Equal(2, result.Length); + Assert.Equal("ModuleA", result[0]); + Assert.Equal("ModuleB", result[1]); + } + + [Fact] + public void FailsWithSimpleCycle() + { + var ex = Assert.Throws(() => + { + solver.AddModule("ModuleB"); + solver.AddDependency("ModuleB", "ModuleB"); + string[] result = solver.Solve(); + }); + + } + + [Fact] + public void CanSolveForest() + { + solver.AddModule("ModuleA"); + solver.AddModule("ModuleB"); + solver.AddModule("ModuleC"); + solver.AddModule("ModuleD"); + solver.AddModule("ModuleE"); + solver.AddModule("ModuleF"); + solver.AddDependency("ModuleC", "ModuleB"); + solver.AddDependency("ModuleB", "ModuleA"); + solver.AddDependency("ModuleE", "ModuleD"); + string[] result = solver.Solve(); + Assert.Equal(6, result.Length); + List test = new List(result); + Assert.True(test.IndexOf("ModuleA") < test.IndexOf("ModuleB")); + Assert.True(test.IndexOf("ModuleB") < test.IndexOf("ModuleC")); + Assert.True(test.IndexOf("ModuleD") < test.IndexOf("ModuleE")); + } + + [Fact] + public void FailsWithComplexCycle() + { + var ex = Assert.Throws(() => + { + solver.AddModule("ModuleA"); + solver.AddModule("ModuleB"); + solver.AddModule("ModuleC"); + solver.AddModule("ModuleD"); + solver.AddModule("ModuleE"); + solver.AddModule("ModuleF"); + solver.AddDependency("ModuleC", "ModuleB"); + solver.AddDependency("ModuleB", "ModuleA"); + solver.AddDependency("ModuleE", "ModuleD"); + solver.AddDependency("ModuleE", "ModuleC"); + solver.AddDependency("ModuleF", "ModuleE"); + solver.AddDependency("ModuleD", "ModuleF"); + solver.AddDependency("ModuleB", "ModuleD"); + solver.Solve(); + }); + + } + + [Fact] + public void FailsWithMissingModule() + { + var ex = Assert.Throws(() => + { + solver.AddModule("ModuleA"); + solver.AddDependency("ModuleA", "ModuleB"); + solver.Solve(); + }); + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupExtensionsFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupExtensionsFixture.cs new file mode 100644 index 0000000000..fafce46773 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupExtensionsFixture.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using Prism.Ioc; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + /// + /// Summary description for ModuleInfoGroupExtensionsFixture + /// + + public class ModuleInfoGroupExtensionsFixture + { + [Fact] + public void ShouldAddModuleToModuleInfoGroup() + { + string moduleName = "MockModule"; + ModuleInfoGroup groupInfo = new ModuleInfoGroup(); + groupInfo.AddModule(moduleName, typeof(MockModule)); + + Assert.Single(groupInfo); + Assert.Equal(moduleName, groupInfo.ElementAt(0).ModuleName); + } + + [Fact] + public void ShouldSetModuleTypeCorrectly() + { + ModuleInfoGroup groupInfo = new ModuleInfoGroup(); + groupInfo.AddModule("MockModule", typeof(MockModule)); + + Assert.Single(groupInfo); + Assert.Equal(typeof(MockModule).AssemblyQualifiedName, groupInfo.ElementAt(0).ModuleType); + } + + [Fact] + public void NullTypeThrows() + { + var ex = Assert.Throws(() => + { + ModuleInfoGroup groupInfo = new ModuleInfoGroup(); + groupInfo.AddModule("NullModule", null); + }); + } + + [Fact] + public void ShouldSetDependencies() + { + string dependency1 = "ModuleA"; + string dependency2 = "ModuleB"; + + ModuleInfoGroup groupInfo = new ModuleInfoGroup(); + groupInfo.AddModule("MockModule", typeof(MockModule), dependency1, dependency2); + + Assert.NotNull(groupInfo.ElementAt(0).DependsOn); + Assert.Equal(2, groupInfo.ElementAt(0).DependsOn.Count); + Assert.Contains(dependency1, groupInfo.ElementAt(0).DependsOn); + Assert.Contains(dependency2, groupInfo.ElementAt(0).DependsOn); + } + + [Fact] + public void ShouldUseTypeNameIfNoNameSpecified() + { + ModuleInfoGroup groupInfo = new ModuleInfoGroup(); + groupInfo.AddModule(typeof(MockModule)); + + Assert.Single(groupInfo); + Assert.Equal(typeof(MockModule).Name, groupInfo.ElementAt(0).ModuleName); + } + + public class MockModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupFixture.cs new file mode 100644 index 0000000000..c09846d529 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupFixture.cs @@ -0,0 +1,21 @@ +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ModuleInfoGroupFixture + { + [Fact] + public void ShouldForwardValuesToModuleInfo() + { + ModuleInfoGroup group = new ModuleInfoGroup(); + group.Ref = "MyCustomGroupRef"; + ModuleInfo moduleInfo = new ModuleInfo(); + Assert.Null(moduleInfo.Ref); + + group.Add(moduleInfo); + + Assert.Equal(group.Ref, moduleInfo.Ref); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInitializerFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInitializerFixture.cs new file mode 100644 index 0000000000..e31fa029dd --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInitializerFixture.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Avalonia.Tests.Mocks; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + /// + /// Summary description for ModuleInitializerFixture + /// + public class ModuleInitializerFixture + { + [Fact] + public void NullContainerThrows() + { + var ex = Assert.Throws(() => + { + ModuleInitializer loader = new ModuleInitializer(null); + }); + } + + [Fact] + public void InitializationExceptionsAreWrapped() + { + var ex = Assert.Throws(() => + { + var moduleInfo = CreateModuleInfo(typeof(ExceptionThrowingModule)); + + ModuleInitializer loader = new ModuleInitializer(new MockContainerAdapter()); + + loader.Initialize(moduleInfo); + }); + } + + [Fact] + public void ShouldResolveModuleAndInitializeSingleModule() + { + IContainerExtension containerFacade = new MockContainerAdapter(); + var service = new ModuleInitializer(containerFacade); + FirstTestModule.wasInitializedOnce = false; + var info = CreateModuleInfo(typeof(FirstTestModule)); + service.Initialize(info); + Assert.True(FirstTestModule.wasInitializedOnce); + } + + [Fact] + public void ShouldLogModuleInitializeErrorsAndContinueLoading() + { + IContainerExtension containerFacade = new MockContainerAdapter(); + var service = new CustomModuleInitializerService(containerFacade); + var invalidModule = CreateModuleInfo(typeof(InvalidModule)); + + Assert.False(service.HandleModuleInitializerrorCalled); + service.Initialize(invalidModule); + Assert.True(service.HandleModuleInitializerrorCalled); + } + + [Fact] + public void ShouldLogModuleInitializationError() + { + IContainerExtension containerFacade = new MockContainerAdapter(); + var service = new ModuleInitializer(containerFacade); + ExceptionThrowingModule.wasInitializedOnce = false; + var exceptionModule = CreateModuleInfo(typeof(ExceptionThrowingModule)); + + try + { + service.Initialize(exceptionModule); + } + catch (ModuleInitializeException mie) + { + Assert.Contains("ExceptionThrowingModule", mie.Message); + } + } + + [Fact] + public void ShouldThrowExceptionIfBogusType() + { + var moduleInfo = new ModuleInfo("TestModule", "BadAssembly.BadType"); + + ModuleInitializer loader = new ModuleInitializer(new MockContainerAdapter()); + + try + { + loader.Initialize(moduleInfo); + //Assert.Fail("Did not throw exception"); + } + catch (ModuleInitializeException ex) + { + Assert.Contains("BadAssembly.BadType", ex.Message); + } + catch (Exception) + { + //Assert.Fail(); + } + } + + private static ModuleInfo CreateModuleInfo(Type type, params string[] dependsOn) + { + ModuleInfo moduleInfo = new ModuleInfo(type.Name, type.AssemblyQualifiedName); + moduleInfo.DependsOn.AddRange(dependsOn); + return moduleInfo; + } + + public static class ModuleLoadTracker + { + public static readonly Stack ModuleLoadStack = new Stack(); + } + + public class FirstTestModule : IModule + { + public static bool wasInitializedOnce; + + public void OnInitialized(IContainerProvider containerProvider) + { + wasInitializedOnce = true; + ModuleLoadTracker.ModuleLoadStack.Push(GetType()); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + + public class SecondTestModule : IModule + { + public static bool wasInitializedOnce; + public static long initializedOnTickCount; + + public void OnInitialized(IContainerProvider containerProvider) + { + wasInitializedOnce = true; + ModuleLoadTracker.ModuleLoadStack.Push(GetType()); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + + public class DependantModule : IModule + { + public static bool wasInitializedOnce; + + public void OnInitialized(IContainerProvider containerProvider) + { + wasInitializedOnce = true; + ModuleLoadTracker.ModuleLoadStack.Push(GetType()); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + + public class DependencyModule : IModule + { + public static bool wasInitializedOnce; + public static long initializedOnTickCount; + + public void OnInitialized(IContainerProvider containerProvider) + { + wasInitializedOnce = true; + ModuleLoadTracker.ModuleLoadStack.Push(GetType()); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } + + public class ExceptionThrowingModule : IModule + { + public static bool wasInitializedOnce; + public static long initializedOnTickCount; + + public void OnInitialized(IContainerProvider containerProvider) + { + throw new InvalidOperationException("Intialization can't be performed"); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } + + public class InvalidModule { } + + public class CustomModuleInitializerService : ModuleInitializer + { + public bool HandleModuleInitializerrorCalled; + + public CustomModuleInitializerService(IContainerExtension containerFacade) + : base(containerFacade) + { + } + + public override void HandleModuleInitializationError(IModuleInfo moduleInfo, string assemblyName, Exception exception) + { + HandleModuleInitializerrorCalled = true; + } + } + + public class Module1 : IModule + { + void IModule.OnInitialized(IContainerProvider containerProvider) { } + void IModule.RegisterTypes(IContainerRegistry containerRegistry) { } + } + public class Module2 : IModule + { + void IModule.OnInitialized(IContainerProvider containerProvider) { } + void IModule.RegisterTypes(IContainerRegistry containerRegistry) { } + } + public class Module3 : IModule + { + void IModule.OnInitialized(IContainerProvider containerProvider) { } + void IModule.RegisterTypes(IContainerRegistry containerRegistry) { } + } + public class Module4 : IModule + { + void IModule.OnInitialized(IContainerProvider containerProvider) { } + void IModule.RegisterTypes(IContainerRegistry containerRegistry) { } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleManagerFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleManagerFixture.cs new file mode 100644 index 0000000000..664415bf1d --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleManagerFixture.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Ioc; +using Prism.Modularity; +using Xunit; + +namespace Prism.Avalonia.Tests.Modularity +{ + public class ModuleManagerFixture + { + [Fact] + public void NullLoaderThrows() + { + var ex = Assert.Throws(() => + { + new ModuleManager(null, new MockModuleCatalog()); + }); + } + + [Fact] + public void NullCatalogThrows() + { + var ex = Assert.Throws(() => + { + new ModuleManager(new MockModuleInitializer(), null); + }); + } + + [Fact] + public void ShouldInvokeRetrieverForModules() + { + var loader = new MockModuleInitializer(); + var moduleInfo = CreateModuleInfo("needsRetrieval", InitializationMode.WhenAvailable); + var catalog = new MockModuleCatalog { Modules = { moduleInfo } }; + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + + manager.Run(); + + Assert.Contains(moduleInfo, moduleTypeLoader.LoadedModules); + } + + [Fact] + public void ShouldInitializeModulesOnRetrievalCompleted() + { + var loader = new MockModuleInitializer(); + var backgroungModuleInfo = CreateModuleInfo("NeedsRetrieval", InitializationMode.WhenAvailable); + var catalog = new MockModuleCatalog { Modules = { backgroungModuleInfo } }; + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + Assert.False(loader.InitializeCalled); + + manager.Run(); + + Assert.True(loader.InitializeCalled); + Assert.Single(loader.InitializedModules); + Assert.Equal(backgroungModuleInfo, loader.InitializedModules[0]); + } + + [Fact] + public void ShouldInitializeModuleOnDemand() + { + var loader = new MockModuleInitializer(); + var onDemandModule = CreateModuleInfo("NeedsRetrieval", InitializationMode.OnDemand); + var catalog = new MockModuleCatalog { Modules = { onDemandModule } }; + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleRetriever = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleRetriever }; + manager.Run(); + + Assert.False(loader.InitializeCalled); + Assert.Empty(moduleRetriever.LoadedModules); + + manager.LoadModule("NeedsRetrieval"); + + Assert.Single(moduleRetriever.LoadedModules); + Assert.True(loader.InitializeCalled); + Assert.Single(loader.InitializedModules); + Assert.Equal(onDemandModule, loader.InitializedModules[0]); + } + + [Fact] + public void InvalidOnDemandModuleNameThrows() + { + var ex = Assert.Throws(() => + { + var loader = new MockModuleInitializer(); + + var catalog = new MockModuleCatalog { Modules = new List { CreateModuleInfo("Missing", InitializationMode.OnDemand) } }; + + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + manager.Run(); + + manager.LoadModule("NonExistent"); + }); + } + + [Fact] + public void EmptyOnDemandModuleReturnedThrows() + { + var ex = Assert.Throws(() => + { + var loader = new MockModuleInitializer(); + + var catalog = new MockModuleCatalog { CompleteListWithDependencies = modules => new List() }; + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleRetriever = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleRetriever }; + manager.Run(); + + manager.LoadModule("NullModule"); + }); + } + + [Fact] + public void ShouldNotLoadTypeIfModuleInitialized() + { + var loader = new MockModuleInitializer(); + var alreadyPresentModule = CreateModuleInfo(typeof(MockModule), InitializationMode.WhenAvailable); + alreadyPresentModule.State = ModuleState.ReadyForInitialization; + var catalog = new MockModuleCatalog { Modules = { alreadyPresentModule } }; + var manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + + manager.Run(); + + Assert.DoesNotContain(alreadyPresentModule, moduleTypeLoader.LoadedModules); + Assert.True(loader.InitializeCalled); + Assert.Single(loader.InitializedModules); + Assert.Equal(alreadyPresentModule, loader.InitializedModules[0]); + } + + [Fact] + public void ShouldNotLoadSameModuleTwice() + { + var loader = new MockModuleInitializer(); + var onDemandModule = CreateModuleInfo(typeof(MockModule), InitializationMode.OnDemand); + var catalog = new MockModuleCatalog { Modules = { onDemandModule } }; + var manager = new ModuleManager(loader, catalog); + manager.Run(); + manager.LoadModule("MockModule"); + loader.InitializeCalled = false; + manager.LoadModule("MockModule"); + + Assert.False(loader.InitializeCalled); + } + + [Fact] + public void ShouldNotLoadModuleThatNeedsRetrievalTwice() + { + var loader = new MockModuleInitializer(); + var onDemandModule = CreateModuleInfo("ModuleThatNeedsRetrieval", InitializationMode.OnDemand); + var catalog = new MockModuleCatalog { Modules = { onDemandModule } }; + var manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + manager.Run(); + manager.LoadModule("ModuleThatNeedsRetrieval"); + moduleTypeLoader.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(onDemandModule, null)); + loader.InitializeCalled = false; + + manager.LoadModule("ModuleThatNeedsRetrieval"); + + Assert.False(loader.InitializeCalled); + } + + [Fact] + public void ShouldCallValidateCatalogBeforeGettingGroupsFromCatalog() + { + var loader = new MockModuleInitializer(); + var catalog = new MockModuleCatalog(); + var manager = new ModuleManager(loader, catalog); + bool validateCatalogCalled = false; + bool getModulesCalledBeforeValidate = false; + + catalog.ValidateCatalog = () => validateCatalogCalled = true; + catalog.CompleteListWithDependencies = f => + { + if (!validateCatalogCalled) + { + getModulesCalledBeforeValidate = true; + } + + return null; + }; + manager.Run(); + + Assert.True(validateCatalogCalled); + Assert.False(getModulesCalledBeforeValidate); + } + + [Fact] + public void ShouldNotInitializeIfDependenciesAreNotMet() + { + var loader = new MockModuleInitializer(); + var requiredModule = CreateModuleInfo("ModuleThatNeedsRetrieval1", InitializationMode.WhenAvailable); + requiredModule.ModuleName = "RequiredModule"; + var dependantModuleInfo = CreateModuleInfo("ModuleThatNeedsRetrieval2", InitializationMode.WhenAvailable, "RequiredModule"); + + var catalog = new MockModuleCatalog { Modules = { requiredModule, dependantModuleInfo } }; + catalog.GetDependentModules = m => new[] { requiredModule }; + + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + + manager.Run(); + + moduleTypeLoader.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(dependantModuleInfo, null)); + + Assert.False(loader.InitializeCalled); + Assert.Empty(loader.InitializedModules); + } + + [Fact] + public void ShouldInitializeIfDependenciesAreMet() + { + var initializer = new MockModuleInitializer(); + var requiredModule = CreateModuleInfo("ModuleThatNeedsRetrieval1", InitializationMode.WhenAvailable); + requiredModule.ModuleName = "RequiredModule"; + var dependantModuleInfo = CreateModuleInfo("ModuleThatNeedsRetrieval2", InitializationMode.WhenAvailable, "RequiredModule"); + + var catalog = new MockModuleCatalog { Modules = { requiredModule, dependantModuleInfo } }; + catalog.GetDependentModules = delegate (IModuleInfo module) + { + if (module == dependantModuleInfo) + return new[] { requiredModule }; + else + return null; + }; + + ModuleManager manager = new ModuleManager(initializer, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + + manager.Run(); + + Assert.True(initializer.InitializeCalled); + Assert.Equal(2, initializer.InitializedModules.Count); + } + + [Fact] + public void ShouldThrowOnRetrieverErrorAndWrapException() + { + var loader = new MockModuleInitializer(); + var moduleInfo = CreateModuleInfo("NeedsRetrieval", InitializationMode.WhenAvailable); + var catalog = new MockModuleCatalog { Modules = { moduleInfo } }; + ModuleManager manager = new ModuleManager(loader, catalog); + var moduleTypeLoader = new MockModuleTypeLoader(); + + Exception retrieverException = new Exception(); + moduleTypeLoader.LoadCompletedError = retrieverException; + + manager.ModuleTypeLoaders = new List { moduleTypeLoader }; + Assert.False(loader.InitializeCalled); + + try + { + manager.Run(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Equal(moduleInfo.ModuleName, ((ModularityException)ex).ModuleName); + Assert.Contains(moduleInfo.ModuleName, ex.Message); + Assert.Same(retrieverException, ex.InnerException); + return; + } + + //Assert.Fail("Exception not thrown."); + } + + [Fact] + public void ShouldThrowIfNoRetrieverCanRetrieveModule() + { + var ex = Assert.Throws(() => + { + + var loader = new MockModuleInitializer(); + var catalog = new MockModuleCatalog { Modules = { CreateModuleInfo("ModuleThatNeedsRetrieval", InitializationMode.WhenAvailable) } }; + ModuleManager manager = new ModuleManager(loader, catalog) + { + ModuleTypeLoaders = new List { new MockModuleTypeLoader() { canLoadModuleTypeReturnValue = false } } + }; + manager.Run(); + }); + } + + [Fact] + public void ShouldWorkIfModuleLoadsAnotherOnDemandModuleWhenInitializing() + { + var initializer = new StubModuleInitializer(); + var onDemandModule = CreateModuleInfo(typeof(MockModule), InitializationMode.OnDemand); + onDemandModule.ModuleName = "OnDemandModule"; + var moduleThatLoadsOtherModule = CreateModuleInfo(typeof(MockModule), InitializationMode.WhenAvailable); + var catalog = new MockModuleCatalog { Modules = { moduleThatLoadsOtherModule, onDemandModule } }; + ModuleManager manager = new ModuleManager(initializer, catalog); + + bool onDemandModuleWasInitialized = false; + initializer.Initialize = m => + { + if (m == moduleThatLoadsOtherModule) + { + manager.LoadModule("OnDemandModule"); + } + else if (m == onDemandModule) + { + onDemandModuleWasInitialized = true; + } + }; + + manager.Run(); + + Assert.True(onDemandModuleWasInitialized); + } + + [Fact] + public void ModuleManagerIsDisposable() + { + Mock mockInit = new Mock(); + var moduleInfo = CreateModuleInfo("needsRetrieval", InitializationMode.WhenAvailable); + var catalog = new Mock(); + ModuleManager manager = new ModuleManager(mockInit.Object, catalog.Object); + + IDisposable disposableManager = manager as IDisposable; + Assert.NotNull(disposableManager); + } + + [Fact] + public void DisposeDoesNotThrowWithNonDisposableTypeLoaders() + { + Mock mockInit = new Mock(); + var moduleInfo = CreateModuleInfo("needsRetrieval", InitializationMode.WhenAvailable); + var catalog = new Mock(); + ModuleManager manager = new ModuleManager(mockInit.Object, catalog.Object); + + var mockTypeLoader = new Mock(); + manager.ModuleTypeLoaders = new List { mockTypeLoader.Object }; + + try + { + manager.Dispose(); + } + catch (Exception) + { + //Assert.Fail(); + } + } + + [Fact] + public void DisposeCleansUpDisposableTypeLoaders() + { + Mock mockInit = new Mock(); + var moduleInfo = CreateModuleInfo("needsRetrieval", InitializationMode.WhenAvailable); + var catalog = new Mock(); + ModuleManager manager = new ModuleManager(mockInit.Object, catalog.Object); + + var mockTypeLoader = new Mock(); + var disposableMockTypeLoader = mockTypeLoader.As(); + disposableMockTypeLoader.Setup(loader => loader.Dispose()); + + manager.ModuleTypeLoaders = new List { mockTypeLoader.Object }; + + manager.Dispose(); + + disposableMockTypeLoader.Verify(loader => loader.Dispose(), Times.Once()); + } + + [Fact] + public void DisposeDoesNotThrowWithMixedTypeLoaders() + { + Mock mockInit = new Mock(); + var moduleInfo = CreateModuleInfo("needsRetrieval", InitializationMode.WhenAvailable); + var catalog = new Mock(); + ModuleManager manager = new ModuleManager(mockInit.Object, catalog.Object); + + var mockTypeLoader1 = new Mock(); + + var mockTypeLoader = new Mock(); + var disposableMockTypeLoader = mockTypeLoader.As(); + disposableMockTypeLoader.Setup(loader => loader.Dispose()); + + manager.ModuleTypeLoaders = new List() { mockTypeLoader1.Object, mockTypeLoader.Object }; + + try + { + manager.Dispose(); + } + catch (Exception) + { + //Assert.Fail(); + } + + disposableMockTypeLoader.Verify(loader => loader.Dispose(), Times.Once()); + } + private static ModuleInfo CreateModuleInfo(string name, InitializationMode initializationMode, params string[] dependsOn) + { + ModuleInfo moduleInfo = new ModuleInfo(name, name) + { + InitializationMode = initializationMode + }; + moduleInfo.DependsOn.AddRange(dependsOn); + return moduleInfo; + } + + private static ModuleInfo CreateModuleInfo(Type type, InitializationMode initializationMode, params string[] dependsOn) + { + ModuleInfo moduleInfo = new ModuleInfo(type.Name, type.AssemblyQualifiedName) + { + InitializationMode = initializationMode + }; + moduleInfo.DependsOn.AddRange(dependsOn); + return moduleInfo; + } + } + + internal class MockModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + throw new NotImplementedException(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new NotImplementedException(); + } + } + + internal class MockModuleCatalog : IModuleCatalog + { + public List Modules = new List(); + public Func> GetDependentModules; + + public Func, IEnumerable> CompleteListWithDependencies; + public Action ValidateCatalog; + + public void Initialize() + { + this.ValidateCatalog?.Invoke(); + } + + IEnumerable IModuleCatalog.Modules => Modules; + + IEnumerable IModuleCatalog.GetDependentModules(IModuleInfo moduleInfo) + { + if (GetDependentModules == null) + return new List(); + + return GetDependentModules(moduleInfo); + } + + IEnumerable IModuleCatalog.CompleteListWithDependencies(IEnumerable modules) + { + if (CompleteListWithDependencies != null) + return CompleteListWithDependencies(modules); + return modules; + } + + public IModuleCatalog AddModule(IModuleInfo moduleInfo) + { + this.Modules.Add(moduleInfo); + return this; + } + } + + internal class MockModuleInitializer : IModuleInitializer + { + public bool InitializeCalled; + public List InitializedModules = new List(); + + public void Initialize(IModuleInfo moduleInfo) + { + InitializeCalled = true; + this.InitializedModules.Add(moduleInfo); + } + } + + internal class StubModuleInitializer : IModuleInitializer + { + public Action Initialize; + + void IModuleInitializer.Initialize(IModuleInfo moduleInfo) + { + this.Initialize((ModuleInfo)moduleInfo); + } + } + + internal class MockDelegateModuleInitializer : IModuleInitializer + { + public Action LoadBody; + + public void Initialize(IModuleInfo moduleInfo) + { + LoadBody((ModuleInfo)moduleInfo); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Modularity/NotAValidDotNetDll.txt.dll b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/NotAValidDotNetDll.txt.dll new file mode 100644 index 0000000000..cc5b4a7864 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/NotAValidDotNetDll.txt.dll @@ -0,0 +1 @@ +This is just a text file renamed as a dll for testing non-.NET DLL loading. diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Mvvm/ViewModelLocatorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Mvvm/ViewModelLocatorFixture.cs new file mode 100644 index 0000000000..c2bfe68508 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mvvm/ViewModelLocatorFixture.cs @@ -0,0 +1,94 @@ +using System; +using System.Reflection; +using Prism.Mvvm; +using Prism.Avalonia.Tests.Mocks.ViewModels; +using Prism.Avalonia.Tests.Mocks.Views; +using Xunit; + +namespace Prism.Avalonia.Tests.Mvvm +{ + public class ViewModelLocatorFixture + { + [StaFact(Skip = "Runs alone but not in a group")] + public void ShouldLocateViewModelWithDefaultSettings() + { + // Warning: flaky test. This runs by itself but not as a whole. + ResetViewModelLocationProvider(); + + Mock view = new Mock(); + Assert.Null(view.DataContext); + + ViewModelLocator.SetAutoWireViewModel(view, true); + Assert.NotNull(view.DataContext); + Assert.IsType(view.DataContext); + } + + [StaFact] + public void ShouldLocateViewModelWithDefaultSettingsForViewsThatEndWithView() + { + ResetViewModelLocationProvider(); + + MockView view = new MockView(); + Assert.Null(view.DataContext); + + ViewModelLocator.SetAutoWireViewModel(view, true); + Assert.NotNull(view.DataContext); + Assert.IsType(view.DataContext); + } + + [StaFact] + public void ShouldUseCustomDefaultViewModelFactoryWhenSet() + { + ResetViewModelLocationProvider(); + + Mock view = new Mock(); + Assert.Null(view.DataContext); + + object mockObject = new object(); + ViewModelLocationProvider.SetDefaultViewModelFactory(viewType => mockObject); + + ViewModelLocator.SetAutoWireViewModel(view, true); + Assert.NotNull(view.DataContext); + ReferenceEquals(view.DataContext, mockObject); + } + + [StaFact] + public void ShouldUseCustomDefaultViewTypeToViewModelTypeResolverWhenSet() + { + ResetViewModelLocationProvider(); + + Mock view = new Mock(); + Assert.Null(view.DataContext); + + ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(viewType => typeof(ViewModelLocatorFixture)); + + ViewModelLocator.SetAutoWireViewModel(view, true); + Assert.NotNull(view.DataContext); + Assert.IsType(view.DataContext); + } + + [StaFact] + public void ShouldUseCustomFactoryWhenSet() + { + ResetViewModelLocationProvider(); + + Mock view = new Mock(); + Assert.Null(view.DataContext); + + string viewModel = "Test String"; + ViewModelLocationProvider.Register(view.GetType().ToString(), () => viewModel); + + ViewModelLocator.SetAutoWireViewModel(view, true); + Assert.NotNull(view.DataContext); + ReferenceEquals(view.DataContext, viewModel); + } + + internal static void ResetViewModelLocationProvider() + { + Type staticType = typeof(ViewModelLocationProvider); + ConstructorInfo ci = staticType.TypeInitializer; + object[] parameters = new object[0]; + ci.Invoke(null, parameters); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Prism.Avalonia.Tests.csproj b/tests/Avalonia/Prism.Avalonia.Tests/Prism.Avalonia.Tests.csproj new file mode 100644 index 0000000000..96e305474e --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Prism.Avalonia.Tests.csproj @@ -0,0 +1,49 @@ + + + + net8.0 + false + enable + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + MSBuild:Compile + + + + diff --git a/tests/Avalonia/Prism.Avalonia.Tests/PrismApplicationBaseFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/PrismApplicationBaseFixture.cs new file mode 100644 index 0000000000..26f7e4e214 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/PrismApplicationBaseFixture.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Styling; +using Moq; +using Prism.Events; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Dialogs; +using Xunit; +using Prism.Navigation.Regions.Behaviors; +using Prism.Navigation.Regions; + +namespace Prism.Avalonia.Tests +{ + /// Application Base Fixture + /// + /// TODO: + /// - Application.Shutdown(); + /// + public class PrismApplicationSetup : IDisposable + { + public PrismApplication Application { get; set; } + + public PrismApplicationSetup() + { + ContainerLocator.ResetContainer(); + Application = new PrismApplication(); + Application.Initialize(); + } + + public void Dispose() + { + ContainerLocator.ResetContainer(); + //// WPF: Application.Shutdown(); + } + } + + public class PrismApplicationBaseFixture : IClassFixture + { + PrismApplication application = null; + + public PrismApplicationBaseFixture(PrismApplicationSetup setup) + { + application = setup.Application; + } + + [Fact] + public void applicationShouldCallConfigureViewModelLocator() + { + Assert.True(application.ConfigureViewModelLocatorWasCalled); + } + + [Fact] + public void applicationShouldCallInitialize() + { + Assert.True(application.InitializeCalled); + } + + [Fact] + public void applicationShouldCallCreateContainerExtension() + { + Assert.True(application.CreateContainerExtensionCalled); + } + + [Fact] + public void applicationShouldCallCreateModuleCatalog() + { + Assert.True(application.CreateModuleCatalogCalled); + } + + [Fact] + public void applicationShouldCallRegisterRequiredTypes() + { + Assert.True(application.RegisterRequiredTypesCalled); + } + + [Fact] + public void applicationShouldCallRegisterTypes() + { + Assert.True(application.RegisterTypesWasCalled); + } + + [Fact] + public void applicationShouldCallConfigureDefaultRegionBehaviors() + { + Assert.True(application.ConfigureDefaultRegionBehaviorsCalled); + } + + [Fact] + public void applicationShouldCallConfigureRegionAdapterMappings() + { + Assert.True(application.ConfigureRegionAdapterMappingsCalled); + } + + [Fact] + public void applicationShouldCallRegisterFrameworkExceptionTypes() + { + Assert.True(application.RegisterFrameworkExceptionTypesCalled); + } + + [Fact] + public void applicationShouldCallCreateShell() + { + Assert.True(application.CreateShellWasCalled); + } + + [Fact] + public void applicationShouldCallInitializeShell() + { + //in our mock Shell is null, so this INitializeShell should not be called by the application + Assert.False(application.InitializeShellWasCalled); + } + + [Fact] + public void applicationShouldCallOnInitialized() + { + Assert.True(application.OnInitializedWasCalled); + } + + [Fact] + public void applicationShouldCallConfigureModuleCatalog() + { + Assert.True(application.ConfigureModuleCatalogCalled); + } + + [Fact] + public void applicationShouldCallInitializeModules() + { + Assert.True(application.InitializeModulesCalled); + } + + [Fact] + public void CreateModuleCatalogShouldReturnDefaultModuleCatalog() + { + Assert.NotNull(application.DefaultModuleCatalog); + } + + [Fact] + public void ConfigureRegionAdapterMappingsShouldRegisterItemsControlMapping() + { + Assert.NotNull(application.DefaultRegionAdapterMappings); + Assert.NotNull(application.DefaultRegionAdapterMappings.GetMapping(typeof(ItemsControl))); + } + + [Fact(Skip = "Selector is currently not implemented.")] + public void ConfigureRegionAdapterMappingsShouldRegisterSelectorMapping() + { + Assert.NotNull(application.DefaultRegionAdapterMappings); + Assert.NotNull(application.DefaultRegionAdapterMappings.GetMapping(typeof(Selector))); + } + + [Fact] + public void ConfigureRegionAdapterMappingsShouldRegisterContentControlMapping() + { + Assert.NotNull(application.DefaultRegionAdapterMappings); + Assert.NotNull(application.DefaultRegionAdapterMappings.GetMapping(typeof(ContentControl))); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddAutoPopulateRegionBehavior() + { + Assert.True(application.DefaultRegionBehaviorTypes.ContainsKey(AutoPopulateRegionBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldBindRegionContextToAvaloniaObjectBehavior() + { + Assert.True(application.DefaultRegionBehaviorTypes.ContainsKey(BindRegionContextToAvaloniaObjectBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddRegionActiveAwareBehavior() + { + Assert.True(application.DefaultRegionBehaviorTypes.ContainsKey(RegionActiveAwareBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddSyncRegionContextWithHostBehavior() + { + Assert.True(application.DefaultRegionBehaviorTypes.ContainsKey(SyncRegionContextWithHostBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddRegionManagerRegistrationBehavior() + { + Assert.True(application.DefaultRegionBehaviorTypes.ContainsKey(RegionManagerRegistrationBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddRegionLifetimeBehavior() + { + Assert.True(application.DefaultRegionBehaviorTypes.ContainsKey(RegionMemberLifetimeBehavior.BehaviorKey)); + } + + [Fact] + public void RequiredTypesAreRegistered() + { + application.MockContainer.Verify(x => x.RegisterInstance(typeof(IModuleCatalog), It.IsAny()), Times.Once); + + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IDialogService), typeof(DialogService)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IModuleInitializer), typeof(ModuleInitializer)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IModuleManager), typeof(ModuleManager)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(RegionAdapterMappings), typeof(RegionAdapterMappings)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionManager), typeof(RegionManager)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionNavigationContentLoader), typeof(RegionNavigationContentLoader)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IEventAggregator), typeof(EventAggregator)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionViewRegistry), typeof(RegionViewRegistry)), Times.Once); + application.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionBehaviorFactory), typeof(RegionBehaviorFactory)), Times.Once); + + application.MockContainer.Verify(x => x.Register(typeof(IRegionNavigationJournalEntry), typeof(RegionNavigationJournalEntry)), Times.Once); + application.MockContainer.Verify(x => x.Register(typeof(IRegionNavigationJournal), typeof(RegionNavigationJournal)), Times.Once); + application.MockContainer.Verify(x => x.Register(typeof(IRegionNavigationService), typeof(RegionNavigationService)), Times.Once); + application.MockContainer.Verify(x => x.Register(typeof(IDialogWindow), typeof(DialogWindow)), Times.Once); + } + } + + public class PrismApplication : PrismApplicationBase + { + public Mock MockContainer { get; private set; } + + public IModuleCatalog DefaultModuleCatalog => Container.Resolve(); + + public IRegionBehaviorFactory DefaultRegionBehaviorTypes => Container.Resolve(); + + public RegionAdapterMappings DefaultRegionAdapterMappings => Container.Resolve(); + + public bool ConfigureViewModelLocatorWasCalled { get; set; } + public bool CreateShellWasCalled { get; set; } + public bool InitializeShellWasCalled { get; set; } + public bool OnInitializedWasCalled { get; set; } + public bool RegisterTypesWasCalled { get; set; } + public bool InitializeModulesCalled { get; internal set; } + public bool ConfigureModuleCatalogCalled { get; internal set; } + public bool RegisterFrameworkExceptionTypesCalled { get; internal set; } + public bool ConfigureRegionAdapterMappingsCalled { get; internal set; } + public bool ConfigureDefaultRegionBehaviorsCalled { get; internal set; } + public bool RegisterRequiredTypesCalled { get; internal set; } + public bool CreateModuleCatalogCalled { get; internal set; } + public bool CreateContainerExtensionCalled { get; internal set; } + public bool InitializeCalled { get; internal set; } + + public override void Initialize() + { + InitializeCalled = true; + + ContainerLocator.ResetContainer(); + MockContainer = new Mock(); + + base.Initialize(); + } + + protected override IContainerExtension CreateContainerExtension() + { + CreateContainerExtensionCalled = true; + return MockContainer.Object; + } + + protected override void ConfigureViewModelLocator() + { + ConfigureViewModelLocatorWasCalled = true; + //setting this breaks other tests using VML. + //We need to revist those tests to ensure it is being reset each time. + //base.ConfigureViewModelLocator(); + } + + protected override IModuleCatalog CreateModuleCatalog() + { + CreateModuleCatalogCalled = true; + + var moduleCatalog = base.CreateModuleCatalog(); + MockContainer.Setup(x => x.Resolve(typeof(IModuleCatalog))).Returns(moduleCatalog); + return moduleCatalog; + } + + protected override Window CreateShell() + { + CreateShellWasCalled = true; + return null; + } + + protected virtual void InitializeShell(Window shell) + { + InitializeShellWasCalled = false; + base.InitializeShell(shell); + } + + protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry) + { + RegisterRequiredTypesCalled = true; + + base.RegisterRequiredTypes(containerRegistry); + + var moduleInitializer = new ModuleInitializer(MockContainer.Object); + MockContainer.Setup(x => x.Resolve(typeof(IModuleInitializer))).Returns(moduleInitializer); + MockContainer.Setup(x => x.Resolve(typeof(IModuleManager))).Returns(new ModuleManager(moduleInitializer, DefaultModuleCatalog)); + MockContainer.Setup(x => x.Resolve(typeof(IRegionBehaviorFactory))).Returns(new RegionBehaviorFactory(MockContainer.Object)); + + var regionBehaviorFactory = new RegionBehaviorFactory(MockContainer.Object); + MockContainer.Setup(x => x.Resolve(typeof(IRegionBehaviorFactory))).Returns(regionBehaviorFactory); + + MockContainer.Setup(x => x.Resolve(typeof(RegionAdapterMappings))).Returns(new RegionAdapterMappings()); + ////MockContainer.Setup(x => x.Resolve(typeof(SelectorRegionAdapter))).Returns(new SelectorRegionAdapter(regionBehaviorFactory)); // From Prism.WPF + MockContainer.Setup(x => x.Resolve(typeof(ItemsControlRegionAdapter))).Returns(new ItemsControlRegionAdapter(regionBehaviorFactory)); + MockContainer.Setup(x => x.Resolve(typeof(ContentControlRegionAdapter))).Returns(new ContentControlRegionAdapter(regionBehaviorFactory)); + } + + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + RegisterTypesWasCalled = true; + } + + protected override void OnInitialized() + { + OnInitializedWasCalled = true; + } + + protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) + { + ConfigureModuleCatalogCalled = true; + base.ConfigureModuleCatalog(moduleCatalog); + } + + protected override void InitializeModules() + { + InitializeModulesCalled = true; + base.InitializeModules(); + } + + protected override void RegisterFrameworkExceptionTypes() + { + RegisterFrameworkExceptionTypesCalled = true; + base.RegisterFrameworkExceptionTypes(); + } + + protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) + { + ConfigureRegionAdapterMappingsCalled = true; + base.ConfigureRegionAdapterMappings(regionAdapterMappings); + } + + protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) + { + ConfigureDefaultRegionBehaviorsCalled = true; + base.ConfigureDefaultRegionBehaviors(regionBehaviors); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/PrismBootstrapperBaseFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/PrismBootstrapperBaseFixture.cs new file mode 100644 index 0000000000..7664fec92a --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/PrismBootstrapperBaseFixture.cs @@ -0,0 +1,348 @@ +using Avalonia; +using Avalonia.Controls; +using Moq; +using Prism.Events; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Dialogs; +using Xunit; +using Prism.Navigation.Regions.Behaviors; +using Prism.Navigation.Regions; +using System; + +namespace Prism +{ + /// Bootstrapper Fixture + /// + /// TODO Items: + /// - public void ConfigureRegionAdapterMappingsShouldRegisterSelectorMapping() ... + /// - MockContainer.Setup(x => x.Resolve(typeof(SelectorRegionAdapter... + /// + public class PrismBootstapperSetup : IDisposable + { + public PrismBootstrapper Bootstrapper { get; set; } + + public PrismBootstapperSetup() + { + ContainerLocator.ResetContainer(); + Bootstrapper = new PrismBootstrapper(); + Bootstrapper.Run(); + } + + public void Dispose() + { + ContainerLocator.ResetContainer(); + } + } + + /// Bootstrapping base fixture + /// This passes when running by itself; fails when ran as a whole. + public class PrismBootstapperBaseFixture : IClassFixture + { + PrismBootstrapper bootstrapper = null; + + public PrismBootstapperBaseFixture(PrismBootstapperSetup setup) + { + bootstrapper = setup.Bootstrapper; + } + + [Fact] + public void BootstrapperShouldCallConfigureViewModelLocator() + { + Assert.True(bootstrapper.ConfigureViewModelLocatorWasCalled); + } + + [Fact] + public void BootstrapperShouldCallInitialize() + { + Assert.True(bootstrapper.InitializeCalled); + } + + [Fact] + public void BootstrapperShouldCallCreateContainerExtension() + { + Assert.True(bootstrapper.CreateContainerExtensionCalled); + } + + [Fact] + public void BootstrapperShouldCallCreateModuleCatalog() + { + Assert.True(bootstrapper.CreateModuleCatalogCalled); + } + + [Fact] + public void BootstrapperShouldCallRegisterRequiredTypes() + { + Assert.True(bootstrapper.RegisterRequiredTypesCalled); + } + + [Fact] + public void BootstrapperShouldCallRegisterTypes() + { + Assert.True(bootstrapper.RegisterTypesWasCalled); + } + + [Fact] + public void BootstrapperShouldCallConfigureDefaultRegionBehaviors() + { + Assert.True(bootstrapper.ConfigureDefaultRegionBehaviorsCalled); + } + + [Fact] + public void BootstrapperShouldCallConfigureRegionAdapterMappings() + { + Assert.True(bootstrapper.ConfigureRegionAdapterMappingsCalled); + } + + [Fact] + public void BootstrapperShouldCallRegisterFrameworkExceptionTypes() + { + Assert.True(bootstrapper.RegisterFrameworkExceptionTypesCalled); + } + + [Fact] + public void BootstrapperShouldCallCreateShell() + { + Assert.True(bootstrapper.CreateShellWasCalled); + } + + [Fact] + public void BootstrapperShouldCallInitializeShell() + { + //in our mock Shell is null, so this INitializeShell should not be called by the bootstrapper + Assert.False(bootstrapper.InitializeShellWasCalled); + } + + [Fact] + public void BootstrapperShouldCallOnInitialized() + { + Assert.True(bootstrapper.OnInitializedWasCalled); + } + + [Fact] + public void BootstrapperShouldCallConfigureModuleCatalog() + { + Assert.True(bootstrapper.ConfigureModuleCatalogCalled); + } + + [Fact] + public void BootstrapperShouldCallInitializeModules() + { + Assert.True(bootstrapper.InitializeModulesCalled); + } + + [Fact] + public void CreateModuleCatalogShouldReturnDefaultModuleCatalog() + { + Assert.NotNull(bootstrapper.DefaultModuleCatalog); + } + + [Fact] + public void ConfigureRegionAdapterMappingsShouldRegisterItemsControlMapping() + { + Assert.NotNull(bootstrapper.DefaultRegionAdapterMappings); + Assert.NotNull(bootstrapper.DefaultRegionAdapterMappings.GetMapping(typeof(ItemsControl))); + } + + ////[Fact] + ////public void ConfigureRegionAdapterMappingsShouldRegisterSelectorMapping() + ////{ + //// Assert.NotNull(bootstrapper.DefaultRegionAdapterMappings); + //// Assert.NotNull(bootstrapper.DefaultRegionAdapterMappings.GetMapping(typeof(Selector))); + ////} + + [Fact] + public void ConfigureRegionAdapterMappingsShouldRegisterContentControlMapping() + { + Assert.NotNull(bootstrapper.DefaultRegionAdapterMappings); + Assert.NotNull(bootstrapper.DefaultRegionAdapterMappings.GetMapping(typeof(ContentControl))); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddAutoPopulateRegionBehavior() + { + Assert.True(bootstrapper.DefaultRegionBehaviorTypes.ContainsKey(AutoPopulateRegionBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldBindRegionContextToAvaloniaObjectBehavior() + { + Assert.True(bootstrapper.DefaultRegionBehaviorTypes.ContainsKey(BindRegionContextToAvaloniaObjectBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddRegionActiveAwareBehavior() + { + Assert.True(bootstrapper.DefaultRegionBehaviorTypes.ContainsKey(RegionActiveAwareBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddSyncRegionContextWithHostBehavior() + { + Assert.True(bootstrapper.DefaultRegionBehaviorTypes.ContainsKey(SyncRegionContextWithHostBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddRegionManagerRegistrationBehavior() + { + Assert.True(bootstrapper.DefaultRegionBehaviorTypes.ContainsKey(RegionManagerRegistrationBehavior.BehaviorKey)); + } + + [Fact] + public void ConfigureDefaultRegionBehaviorsShouldAddRegionLifetimeBehavior() + { + Assert.True(bootstrapper.DefaultRegionBehaviorTypes.ContainsKey(RegionMemberLifetimeBehavior.BehaviorKey)); + } + + [Fact] + public void RequiredTypesAreRegistered() + { + bootstrapper.MockContainer.Verify(x => x.RegisterInstance(typeof(IModuleCatalog), It.IsAny()), Times.Once); + + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IDialogService), typeof(DialogService)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IModuleInitializer), typeof(ModuleInitializer)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IModuleManager), typeof(ModuleManager)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(RegionAdapterMappings), typeof(RegionAdapterMappings)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionManager), typeof(RegionManager)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionNavigationContentLoader), typeof(RegionNavigationContentLoader)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IEventAggregator), typeof(EventAggregator)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionViewRegistry), typeof(RegionViewRegistry)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.RegisterSingleton(typeof(IRegionBehaviorFactory), typeof(RegionBehaviorFactory)), Times.Once); + + bootstrapper.MockContainer.Verify(x => x.Register(typeof(IRegionNavigationJournalEntry), typeof(RegionNavigationJournalEntry)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.Register(typeof(IRegionNavigationJournal), typeof(RegionNavigationJournal)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.Register(typeof(IRegionNavigationService), typeof(RegionNavigationService)), Times.Once); + bootstrapper.MockContainer.Verify(x => x.Register(typeof(IDialogWindow), typeof(DialogWindow)), Times.Once); + } + } + + public class PrismBootstrapper : PrismBootstrapperBase + { + public Mock MockContainer { get; private set; } + + public IModuleCatalog DefaultModuleCatalog => Container.Resolve(); + + public IRegionBehaviorFactory DefaultRegionBehaviorTypes => Container.Resolve(); + + public RegionAdapterMappings DefaultRegionAdapterMappings => Container.Resolve(); + + public bool ConfigureViewModelLocatorWasCalled { get; set; } + public bool CreateShellWasCalled { get; set; } + public bool InitializeShellWasCalled { get; set; } + public bool OnInitializedWasCalled { get; set; } + public bool RegisterTypesWasCalled { get; set; } + public bool InitializeModulesCalled { get; internal set; } + public bool ConfigureModuleCatalogCalled { get; internal set; } + public bool RegisterFrameworkExceptionTypesCalled { get; internal set; } + public bool ConfigureRegionAdapterMappingsCalled { get; internal set; } + public bool ConfigureDefaultRegionBehaviorsCalled { get; internal set; } + public bool RegisterRequiredTypesCalled { get; internal set; } + public bool CreateModuleCatalogCalled { get; internal set; } + public bool CreateContainerExtensionCalled { get; internal set; } + public bool InitializeCalled { get; internal set; } + + protected override void Initialize() + { + InitializeCalled = true; + + ContainerLocator.ResetContainer(); + MockContainer = new Mock(); + + base.Initialize(); + } + + protected override IContainerExtension CreateContainerExtension() + { + CreateContainerExtensionCalled = true; + return MockContainer.Object; + } + + protected override void ConfigureViewModelLocator() + { + ConfigureViewModelLocatorWasCalled = true; + //setting this breaks other tests using VML. + //We need to revist those tests to ensure it is being reset each time. + //base.ConfigureViewModelLocator(); + } + + protected override IModuleCatalog CreateModuleCatalog() + { + CreateModuleCatalogCalled = true; + + var moduleCatalog = base.CreateModuleCatalog(); + MockContainer.Setup(x => x.Resolve(typeof(IModuleCatalog))).Returns(moduleCatalog); + return moduleCatalog; + } + + protected override AvaloniaObject CreateShell() + { + CreateShellWasCalled = true; + return null; + } + + protected override void InitializeShell(AvaloniaObject shell) + { + InitializeShellWasCalled = false; + } + + protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry) + { + RegisterRequiredTypesCalled = true; + + base.RegisterRequiredTypes(containerRegistry); + + var moduleInitializer = new ModuleInitializer(MockContainer.Object); + MockContainer.Setup(x => x.Resolve(typeof(IModuleInitializer))).Returns(moduleInitializer); + MockContainer.Setup(x => x.Resolve(typeof(IModuleManager))).Returns(new ModuleManager(moduleInitializer, DefaultModuleCatalog)); + MockContainer.Setup(x => x.Resolve(typeof(IRegionBehaviorFactory))).Returns(new RegionBehaviorFactory(MockContainer.Object)); + + var regionBehaviorFactory = new RegionBehaviorFactory(MockContainer.Object); + MockContainer.Setup(x => x.Resolve(typeof(IRegionBehaviorFactory))).Returns(regionBehaviorFactory); + + MockContainer.Setup(x => x.Resolve(typeof(RegionAdapterMappings))).Returns(new RegionAdapterMappings()); + //// TODO: MockContainer.Setup(x => x.Resolve(typeof(SelectorRegionAdapter))).Returns(new SelectorRegionAdapter(regionBehaviorFactory)); + MockContainer.Setup(x => x.Resolve(typeof(ItemsControlRegionAdapter))).Returns(new ItemsControlRegionAdapter(regionBehaviorFactory)); + MockContainer.Setup(x => x.Resolve(typeof(ContentControlRegionAdapter))).Returns(new ContentControlRegionAdapter(regionBehaviorFactory)); + } + + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + RegisterTypesWasCalled = true; + } + + protected override void OnInitialized() + { + OnInitializedWasCalled = true; + } + + protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) + { + ConfigureModuleCatalogCalled = true; + base.ConfigureModuleCatalog(moduleCatalog); + } + + protected override void InitializeModules() + { + InitializeModulesCalled = true; + base.InitializeModules(); + } + + protected override void RegisterFrameworkExceptionTypes() + { + RegisterFrameworkExceptionTypesCalled = true; + base.RegisterFrameworkExceptionTypes(); + } + + protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) + { + ConfigureRegionAdapterMappingsCalled = true; + base.ConfigureRegionAdapterMappings(regionAdapterMappings); + } + + protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) + { + ConfigureDefaultRegionBehaviorsCalled = true; + base.ConfigureDefaultRegionBehaviors(regionBehaviors); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/AllActiveRegionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/AllActiveRegionFixture.cs new file mode 100644 index 0000000000..92a475a582 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/AllActiveRegionFixture.cs @@ -0,0 +1,34 @@ +using System; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class AllActiveRegionFixture + { + [Fact] + public void AddingViewsToRegionMarksThemAsActive() + { + IRegion region = new AllActiveRegion(); + var view = new object(); + + region.Add(view); + + Assert.True(region.ActiveViews.Contains(view)); + } + + [Fact] + public void DeactivateThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new AllActiveRegion(); + var view = new object(); + region.Add(view); + + region.Deactivate(view); + }); + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs new file mode 100644 index 0000000000..8693d77499 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class AutoPopulateRegionBehaviorFixture + { + [Fact] + public void ShouldGetViewsFromRegistryOnAttach() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var region = new MockPresentationRegion() { Name = "MyRegion" }; + var viewFactory = new MockRegionContentRegistry(); + var view = new object(); + viewFactory.GetContentsReturnValue.Add(view); + var behavior = new AutoPopulateRegionBehavior(viewFactory) + { + Region = region + }; + + behavior.Attach(); + + Assert.Equal("MyRegion", viewFactory.GetContentsArgumentRegionName); + Assert.Single(region.MockViews.Items); + Assert.Equal(view, region.MockViews.Items[0]); + } + + [Fact] + public void ShouldGetViewsFromRegistryWhenRegisteringItAfterAttach() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var region = new MockPresentationRegion() { Name = "MyRegion" }; + var viewFactory = new MockRegionContentRegistry(); + var behavior = new AutoPopulateRegionBehavior(viewFactory) + { + Region = region + }; + var view = new object(); + + behavior.Attach(); + viewFactory.GetContentsReturnValue.Add(view); + viewFactory.RaiseContentRegistered("MyRegion", view); + + Assert.Equal("MyRegion", viewFactory.GetContentsArgumentRegionName); + Assert.Single(region.MockViews.Items); + Assert.Equal(view, region.MockViews.Items[0]); + } + + [Fact] + public void NullRegionThrows() + { + var ex = Assert.Throws(() => + { + var behavior = new AutoPopulateRegionBehavior(new MockRegionContentRegistry()); + + behavior.Attach(); + }); + + } + + [Fact] + public void CanAttachBeforeSettingName() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + ContainerLocator.SetContainerExtension(Mock.Of()); + var region = new MockPresentationRegion() { Name = null }; + var viewFactory = new MockRegionContentRegistry(); + var view = new object(); + viewFactory.GetContentsReturnValue.Add(view); + var behavior = new AutoPopulateRegionBehavior(viewFactory) + { + Region = region + }; + + behavior.Attach(); + Assert.False(viewFactory.GetContentsCalled); + + region.Name = "MyRegion"; + + Assert.True(viewFactory.GetContentsCalled); + Assert.Equal("MyRegion", viewFactory.GetContentsArgumentRegionName); + Assert.Single(region.MockViews.Items); + Assert.Equal(view, region.MockViews.Items[0]); + } + + private class MockRegionContentRegistry : IRegionViewRegistry + { + public readonly List GetContentsReturnValue = new List(); + public string GetContentsArgumentRegionName; + public bool GetContentsCalled; + + public event EventHandler ContentRegistered; + + public IEnumerable GetContents(string regionName, IContainerProvider container) + { + GetContentsCalled = true; + GetContentsArgumentRegionName = regionName; + return GetContentsReturnValue; + } + + public void RaiseContentRegistered(string regionName, object view) + { + ContentRegistered(this, new ViewRegisteredEventArgs(regionName, _ => view)); + } + + public void RegisterViewWithRegion(string regionName, Type viewType) + { + throw new NotImplementedException(); + } + + public void RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + throw new NotImplementedException(); + } + + public void RegisterViewWithRegion(string regionName, string targetName) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehaviorFixture.cs new file mode 100644 index 0000000000..d61aa88af1 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehaviorFixture.cs @@ -0,0 +1,98 @@ +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class BindRegionContextToAvaloniaObjectBehaviorFixture + { + [StaFact(Skip = "Review: Potentially not supported")] + public void ShouldSetRegionContextOnAddedView() + { + var behavior = new BindRegionContextToAvaloniaObjectBehavior(); + var region = new MockPresentationRegion(); + behavior.Region = region; + region.Context = "MyContext"; + var view = new MockDependencyObject(); + + behavior.Attach(); + region.Add(view); + + var context = RegionContext.GetObservableContext(view); + Assert.NotNull(context.Value); + Assert.Equal("MyContext", context.Value); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void ShouldSetRegionContextOnAlreadyAddedViews() + { + var behavior = new BindRegionContextToAvaloniaObjectBehavior(); + var region = new MockPresentationRegion(); + var view = new MockDependencyObject(); + region.Add(view); + behavior.Region = region; + region.Context = "MyContext"; + + behavior.Attach(); + + var context = RegionContext.GetObservableContext(view); + Assert.NotNull(context.Value); + Assert.Equal("MyContext", context.Value); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void ShouldRemoveContextToViewRemovedFromRegion() + { + var behavior = new BindRegionContextToAvaloniaObjectBehavior(); + var region = new MockPresentationRegion(); + var view = new MockDependencyObject(); + region.Add(view); + behavior.Region = region; + region.Context = "MyContext"; + behavior.Attach(); + + region.Remove(view); + + var context = RegionContext.GetObservableContext(view); + Assert.Null(context.Value); + } + + [StaFact(Skip = "Avalonia doesn't auto-create ObservableObject in RegionContext")] + public void ShouldSetRegionContextOnContextChange() + { + var behavior = new BindRegionContextToAvaloniaObjectBehavior(); + var region = new MockPresentationRegion(); + var view = new MockDependencyObject(); + region.Add(view); + behavior.Region = region; + region.Context = "MyContext"; + behavior.Attach(); + Assert.Equal("MyContext", RegionContext.GetObservableContext(view).Value); + + region.Context = "MyNewContext"; + region.OnPropertyChange("Context"); + + Assert.Equal("MyNewContext", RegionContext.GetObservableContext(view).Value); + } + + [StaFact] + public void WhenAViewIsRemovedFromARegion_ThenRegionContextIsNotClearedInRegion() + { + var behavior = new BindRegionContextToAvaloniaObjectBehavior(); + var region = new MockPresentationRegion(); + + behavior.Region = region; + behavior.Attach(); + + var myView = new MockFrameworkElement(); + + region.Add(myView); + region.Context = "new context"; + + region.Remove(myView); + + Assert.NotNull(region.Context); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/ClearChildViewsRegionBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/ClearChildViewsRegionBehaviorFixture.cs new file mode 100644 index 0000000000..ab7fa9f502 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/ClearChildViewsRegionBehaviorFixture.cs @@ -0,0 +1,80 @@ +using Prism.Navigation.Regions; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class ClearChildViewsRegionBehaviorFixture + { + [StaFact] + public void WhenClearChildViewsPropertyIsNotSet_ThenChildViewsRegionManagerIsNotCleared() + { + var regionManager = new MockRegionManager(); + + var region = new Region(); + region.RegionManager = regionManager; + + var behavior = new ClearChildViewsRegionBehavior(); + behavior.Region = region; + behavior.Attach(); + + var childView = new MockFrameworkElement(); + region.Add(childView); + + Assert.Equal(regionManager, childView.GetValue(RegionManager.RegionManagerProperty)); + + region.RegionManager = null; + + Assert.Equal(regionManager, childView.GetValue(RegionManager.RegionManagerProperty)); + } + + [StaFact] + public void WhenClearChildViewsPropertyIsTrue_ThenChildViewsRegionManagerIsCleared() + { + var regionManager = new MockRegionManager(); + + var region = new Region(); + region.RegionManager = regionManager; + + var behavior = new ClearChildViewsRegionBehavior(); + behavior.Region = region; + behavior.Attach(); + + var childView = new MockFrameworkElement(); + region.Add(childView); + + ClearChildViewsRegionBehavior.SetClearChildViews(childView, true); + + Assert.Equal(regionManager, childView.GetValue(RegionManager.RegionManagerProperty)); + + region.RegionManager = null; + + Assert.Null(childView.GetValue(RegionManager.RegionManagerProperty)); + } + + [StaFact] + public void WhenRegionManagerChangesToNotNullValue_ThenChildViewsRegionManagerIsNotCleared() + { + var regionManager = new MockRegionManager(); + + var region = new Region(); + region.RegionManager = regionManager; + + var behavior = new ClearChildViewsRegionBehavior(); + behavior.Region = region; + behavior.Attach(); + + var childView = new MockFrameworkElement(); + region.Add(childView); + + childView.SetValue(ClearChildViewsRegionBehavior.ClearChildViewsProperty, true); + + Assert.Equal(regionManager, childView.GetValue(RegionManager.RegionManagerProperty)); + + region.RegionManager = new MockRegionManager(); + + Assert.NotNull(childView.GetValue(RegionManager.RegionManagerProperty)); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/DelayedRegionCreationBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/DelayedRegionCreationBehaviorFixture.cs new file mode 100644 index 0000000000..41a6214ff6 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/DelayedRegionCreationBehaviorFixture.cs @@ -0,0 +1,201 @@ +using System; +using System.Linq; +using Avalonia; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class DelayedRegionCreationBehaviorFixture + { + private DelayedRegionCreationBehavior GetBehavior(AvaloniaObject control, MockRegionManagerAccessor accessor, MockRegionAdapter adapter) + { + var mappings = new RegionAdapterMappings(); + mappings.RegisterMapping(control.GetType(), adapter); + var behavior = new DelayedRegionCreationBehavior(mappings); + behavior.RegionManagerAccessor = accessor; + behavior.TargetElement = control; + return behavior; + } + + private DelayedRegionCreationBehavior GetBehavior(AvaloniaObject control, MockRegionManagerAccessor accessor) + { + return GetBehavior(control, accessor, new MockRegionAdapter()); + } + + [StaFact] + public void RegionWillNotGetCreatedTwiceWhenThereAreMoreRegions() + { + var control1 = new MockFrameworkElement(); + var control2 = new MockFrameworkElement(); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => d == control1 ? "Region1" : "Region2" + }; + + var adapter = new MockRegionAdapter(); + adapter.Accessor = accessor; + + var behavior1 = GetBehavior(control1, accessor, adapter); + var behavior2 = GetBehavior(control2, accessor, adapter); + + behavior1.Attach(); + behavior2.Attach(); + + accessor.UpdateRegions(); + + Assert.Contains("Region1", adapter.CreatedRegions); + Assert.Contains("Region2", adapter.CreatedRegions); + Assert.Equal(1, adapter.CreatedRegions.Count((name) => name == "Region2")); + } + + [StaFact] + public void RegionGetsCreatedWhenAccessingRegions() + { + var control1 = new MockFrameworkElement(); + var control2 = new MockFrameworkContentElement(); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior1 = GetBehavior(control1, accessor); + behavior1.Attach(); + var behavior2 = GetBehavior(control2, accessor); + behavior2.Attach(); + + accessor.UpdateRegions(); + + Assert.NotNull(RegionManager.GetObservableRegion(control1).Value); + Assert.IsAssignableFrom(RegionManager.GetObservableRegion(control1).Value); + Assert.NotNull(RegionManager.GetObservableRegion(control2).Value); + Assert.IsAssignableFrom(RegionManager.GetObservableRegion(control2).Value); + } + + [StaFact] + public void RegionDoesNotGetCreatedTwiceWhenUpdatingRegions() + { + var control = new MockFrameworkElement(); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = GetBehavior(control, accessor); + behavior.Attach(); + accessor.UpdateRegions(); + IRegion region = RegionManager.GetObservableRegion(control).Value; + + accessor.UpdateRegions(); + + Assert.Same(region, RegionManager.GetObservableRegion(control).Value); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void BehaviorDoesNotPreventControlFromBeingGarbageCollected() + { + var control = new MockFrameworkElement(); + WeakReference controlWeakReference = new WeakReference(control); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = GetBehavior(control, accessor); + behavior.Attach(); + + Assert.True(controlWeakReference.IsAlive); + GC.KeepAlive(control); + + control = null; + GC.Collect(); + + Assert.False(controlWeakReference.IsAlive); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void BehaviorDoesNotPreventControlFromBeingGarbageCollectedWhenRegionWasCreated() + { + var control = new MockFrameworkElement(); + WeakReference controlWeakReference = new WeakReference(control); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = GetBehavior(control, accessor); + behavior.Attach(); + accessor.UpdateRegions(); + + Assert.True(controlWeakReference.IsAlive); + GC.KeepAlive(control); + + control = null; + GC.Collect(); + + Assert.False(controlWeakReference.IsAlive); + } + + [StaFact] + public void BehaviorShouldUnhookEventWhenDetaching() + { + var control = new MockFrameworkElement(); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName", + }; + var behavior = GetBehavior(control, accessor); + behavior.Attach(); + + int startingCount = accessor.GetSubscribersCount(); + + behavior.Detach(); + + Assert.Equal(startingCount - 1, accessor.GetSubscribersCount()); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void ShouldCleanupBehaviorOnceRegionIsCreated() + { + var control = new MockFrameworkElement(); + var control2 = new MockFrameworkContentElement(); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = GetBehavior(control, accessor); + WeakReference behaviorWeakReference = new WeakReference(behavior); + behavior.Attach(); + accessor.UpdateRegions(); + Assert.True(behaviorWeakReference.IsAlive); + GC.KeepAlive(behavior); + + behavior = null; + GC.Collect(); + + Assert.False(behaviorWeakReference.IsAlive); + + var behavior2 = GetBehavior(control2, accessor); + WeakReference behaviorWeakReference2 = new WeakReference(behavior2); + behavior2.Attach(); + accessor.UpdateRegions(); + Assert.True(behaviorWeakReference2.IsAlive); + GC.KeepAlive(behavior2); + + behavior2 = null; + GC.Collect(); + + Assert.False(behaviorWeakReference2.IsAlive); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionActiveAwareBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionActiveAwareBehaviorFixture.cs new file mode 100644 index 0000000000..9bcf474111 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionActiveAwareBehaviorFixture.cs @@ -0,0 +1,332 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class RegionActiveAwareBehaviorFixture + { + [StaFact] + public void SetsIsActivePropertyOnIActiveAwareObjects() + { + var region = new MockPresentationRegion(); + region.RegionManager = new MockRegionManager(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + var collection = region.MockActiveViews.Items; + + ActiveAwareFrameworkElement activeAwareObject = new ActiveAwareFrameworkElement(); + + Assert.False(activeAwareObject.IsActive); + collection.Add(activeAwareObject); + + Assert.True(activeAwareObject.IsActive); + + collection.Remove(activeAwareObject); + Assert.False(activeAwareObject.IsActive); + } + + [StaFact] + public void SetsIsActivePropertyOnIActiveAwareDataContexts() + { + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + var collection = region.MockActiveViews.Items; + + ActiveAwareFrameworkElement activeAwareObject = new ActiveAwareFrameworkElement(); + + var frameworkElementMock = new Mock(); + var frameworkElement = frameworkElementMock.Object; + frameworkElement.DataContext = activeAwareObject; + + Assert.False(activeAwareObject.IsActive); + collection.Add(frameworkElement); + + Assert.True(activeAwareObject.IsActive); + + collection.Remove(frameworkElement); + Assert.False(activeAwareObject.IsActive); + } + + [StaFact] + public void SetsIsActivePropertyOnBothIActiveAwareViewAndDataContext() + { + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + var collection = region.MockActiveViews.Items; + + var activeAwareMock = new Mock(); + activeAwareMock.SetupProperty(o => o.IsActive); + var activeAwareObject = activeAwareMock.Object; + + var frameworkElementMock = new Mock(); + frameworkElementMock.As().SetupProperty(o => o.IsActive); + var frameworkElement = frameworkElementMock.Object; + frameworkElement.DataContext = activeAwareObject; + + Assert.False(((IActiveAware)frameworkElement).IsActive); + Assert.False(activeAwareObject.IsActive); + collection.Add(frameworkElement); + + Assert.True(((IActiveAware)frameworkElement).IsActive); + Assert.True(activeAwareObject.IsActive); + + collection.Remove(frameworkElement); + Assert.False(((IActiveAware)frameworkElement).IsActive); + Assert.False(activeAwareObject.IsActive); + } + + [StaFact] + public void DetachStopsListeningForChanges() + { + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + var collection = region.MockActiveViews.Items; + behavior.Attach(); + behavior.Detach(); + ActiveAwareFrameworkElement activeAwareObject = new ActiveAwareFrameworkElement(); + + collection.Add(activeAwareObject); + + Assert.False(activeAwareObject.IsActive); + } + + [StaFact] + public void DoesNotThrowWhenAddingNonActiveAwareObjects() + { + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + var collection = region.MockActiveViews.Items; + + collection.Add(new object()); + } + + [StaFact] + public void DoesNotThrowWhenAddingNonActiveAwareDataContexts() + { + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + var collection = region.MockActiveViews.Items; + + var frameworkElementMock = new Mock(); + var frameworkElement = frameworkElementMock.Object; + frameworkElement.DataContext = new object(); + + collection.Add(frameworkElement); + } + + [StaFact] + public void WhenParentViewGetsActivatedOrDeactivated_ThenChildViewIsNotUpdated() + { + var scopedRegionManager = new RegionManager(); + var scopedRegion = new Region { Name = "MyScopedRegion", RegionManager = scopedRegionManager }; + scopedRegionManager.Regions.Add(scopedRegion); + var behaviorForScopedRegion = new RegionActiveAwareBehavior { Region = scopedRegion }; + behaviorForScopedRegion.Attach(); + var childActiveAwareView = new ActiveAwareFrameworkElement(); + + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + + var view = new MockFrameworkElement(); + region.Add(view); + RegionManager.SetRegionManager(view, scopedRegionManager); + region.Activate(view); + + scopedRegion.Add(childActiveAwareView); + scopedRegion.Activate(childActiveAwareView); + + Assert.True(childActiveAwareView.IsActive); + + region.Deactivate(view); + + Assert.True(childActiveAwareView.IsActive); + } + + [StaFact] + public void WhenParentViewGetsActivatedOrDeactivated_ThenSyncedChildViewIsUpdated() + { + var scopedRegionManager = new RegionManager(); + var scopedRegion = new Region { Name = "MyScopedRegion", RegionManager = scopedRegionManager }; + scopedRegionManager.Regions.Add(scopedRegion); + var behaviorForScopedRegion = new RegionActiveAwareBehavior { Region = scopedRegion }; + behaviorForScopedRegion.Attach(); + var childActiveAwareView = new SyncedActiveAwareObject(); + + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + + var view = new MockFrameworkElement(); + region.Add(view); + RegionManager.SetRegionManager(view, scopedRegionManager); + region.Activate(view); + + scopedRegion.Add(childActiveAwareView); + scopedRegion.Activate(childActiveAwareView); + + Assert.True(childActiveAwareView.IsActive); + + region.Deactivate(view); + + Assert.False(childActiveAwareView.IsActive); + } + + [StaFact] + public void WhenParentViewGetsActivatedOrDeactivated_ThenSyncedChildViewWithAttributeInVMIsUpdated() + { + var scopedRegionManager = new RegionManager(); + var scopedRegion = new Region { Name = "MyScopedRegion", RegionManager = scopedRegionManager }; + scopedRegionManager.Regions.Add(scopedRegion); + var behaviorForScopedRegion = new RegionActiveAwareBehavior { Region = scopedRegion }; + behaviorForScopedRegion.Attach(); + var childActiveAwareView = new ActiveAwareFrameworkElement(); + childActiveAwareView.DataContext = new SyncedActiveAwareObjectViewModel(); + + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + + var view = new MockFrameworkElement(); + region.Add(view); + RegionManager.SetRegionManager(view, scopedRegionManager); + region.Activate(view); + + scopedRegion.Add(childActiveAwareView); + scopedRegion.Activate(childActiveAwareView); + + Assert.True(childActiveAwareView.IsActive); + + region.Deactivate(view); + + Assert.False(childActiveAwareView.IsActive); + } + + [StaFact] + public void WhenParentViewGetsActivatedOrDeactivated_ThenSyncedChildViewModelThatIsNotAFrameworkElementIsNotUpdated() + { + var scopedRegionManager = new RegionManager(); + var scopedRegion = new Region { Name = "MyScopedRegion", RegionManager = scopedRegionManager }; + scopedRegionManager.Regions.Add(scopedRegion); + var behaviorForScopedRegion = new RegionActiveAwareBehavior { Region = scopedRegion }; + behaviorForScopedRegion.Attach(); + var childActiveAwareView = new ActiveAwareObject(); + + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + + var view = new MockFrameworkElement(); + region.Add(view); + RegionManager.SetRegionManager(view, scopedRegionManager); + region.Activate(view); + + scopedRegion.Add(childActiveAwareView); + scopedRegion.Activate(childActiveAwareView); + + Assert.True(childActiveAwareView.IsActive); + + region.Deactivate(view); + + Assert.True(childActiveAwareView.IsActive); + } + + [StaFact] + public void WhenParentViewGetsActivatedOrDeactivated_ThenSyncedChildViewNotInActiveViewsIsNotUpdated() + { + var scopedRegionManager = new RegionManager(); + var scopedRegion = new Region { Name = "MyScopedRegion", RegionManager = scopedRegionManager }; + scopedRegionManager.Regions.Add(scopedRegion); + var behaviorForScopedRegion = new RegionActiveAwareBehavior { Region = scopedRegion }; + behaviorForScopedRegion.Attach(); + var childActiveAwareView = new SyncedActiveAwareObject(); + + var region = new MockPresentationRegion(); + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + + var view = new MockFrameworkElement(); + region.Add(view); + RegionManager.SetRegionManager(view, scopedRegionManager); + region.Activate(view); + + scopedRegion.Add(childActiveAwareView); + scopedRegion.Deactivate(childActiveAwareView); + + Assert.False(childActiveAwareView.IsActive); + + region.Deactivate(view); + + Assert.False(childActiveAwareView.IsActive); + + region.Activate(view); + + Assert.False(childActiveAwareView.IsActive); + } + + [StaFact] + public void WhenParentViewWithoutScopedRegionGetsActivatedOrDeactivated_ThenSyncedChildViewIsNotUpdated() + { + var commonRegionManager = new RegionManager(); + var nonScopedRegion = new Region { Name = "MyRegion", RegionManager = commonRegionManager }; + commonRegionManager.Regions.Add(nonScopedRegion); + var behaviorForScopedRegion = new RegionActiveAwareBehavior { Region = nonScopedRegion }; + behaviorForScopedRegion.Attach(); + var childActiveAwareView = new SyncedActiveAwareObject(); + + var region = new MockPresentationRegion { RegionManager = commonRegionManager }; + var behavior = new RegionActiveAwareBehavior { Region = region }; + behavior.Attach(); + + var view = new MockFrameworkElement(); + region.Add(view); + RegionManager.SetRegionManager(view, commonRegionManager); + region.Activate(view); + + nonScopedRegion.Add(childActiveAwareView); + nonScopedRegion.Activate(childActiveAwareView); + + Assert.True(childActiveAwareView.IsActive); + + region.Deactivate(view); + + Assert.True(childActiveAwareView.IsActive); + } + + class ActiveAwareObject : IActiveAware + { + public bool IsActive { get; set; } + public event EventHandler IsActiveChanged; + } + + class ActiveAwareFrameworkElement : Control, IActiveAware + { + public bool IsActive { get; set; } + public event EventHandler IsActiveChanged; + } + + [SyncActiveState] + class SyncedActiveAwareObject : IActiveAware + { + public bool IsActive { get; set; } + public event EventHandler IsActiveChanged; + } + + [SyncActiveState] + class SyncedActiveAwareObjectViewModel : IActiveAware + { + public bool IsActive { get; set; } + public event EventHandler IsActiveChanged; + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionManagerRegistrationBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionManagerRegistrationBehaviorFixture.cs new file mode 100644 index 0000000000..1bc81172bc --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionManagerRegistrationBehaviorFixture.cs @@ -0,0 +1,364 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Prism.Avalonia.Tests.Mocks; +using Prism.Ioc; +using Prism.Navigation; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + /// + /// Region Manager Registration Behavior Fixture tests. + /// + /// + /// The MockFrameworkElement depends on the following: + /// Avalonia.Control's LoadedEvent and UnloadedEvent wont arrive until Avalonia v0.11.0. + /// Discussion: https://github.com/AvaloniaUI/Avalonia/issues/7908 + /// PR: https://github.com/AvaloniaUI/Avalonia/pull/8277 + /// + public class RegionManagerRegistrationBehaviorFixture + { + [StaFact] + public void ShouldRegisterRegionIfRegionManagerIsSet() + { + var control = new ItemsControl(); + var regionManager = new MockRegionManager(); + var accessor = new MockRegionManagerAccessor + { + GetRegionManager = d => regionManager + }; + var region = new MockPresentationRegion() { Name = "myRegionName" }; + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = region, + HostControl = control + }; + + behavior.Attach(); + + Assert.True(regionManager.MockRegionCollection.AddCalled); + Assert.Same(region, regionManager.MockRegionCollection.AddArgument); + } + + [StaFact] + public void DoesNotFailIfRegionManagerIsNotSet() + { + var control = new ItemsControl(); + var accessor = new MockRegionManagerAccessor(); + + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = new MockPresentationRegion() { Name = "myRegionWithoutManager" }, + HostControl = control + }; + behavior.Attach(); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void RegionGetsAddedInRegionManagerWhenAddedIntoAScopeAndAccessingRegions() + { + var regionManager = new MockRegionManager(); + var control = new MockFrameworkElement(); + + var regionScopeControl = new ContentControl(); + var accessor = new MockRegionManagerAccessor + { + GetRegionManager = d => d == regionScopeControl ? regionManager : null + }; + + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = new MockPresentationRegion() { Name = "myRegionName" }, + HostControl = control + }; + behavior.Attach(); + + Assert.False(regionManager.MockRegionCollection.AddCalled); + + regionScopeControl.Content = control; + accessor.UpdateRegions(); + + Assert.True(regionManager.MockRegionCollection.AddCalled); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void RegionDoesNotGetAddedTwiceWhenUpdatingRegions() + { + var regionManager = new MockRegionManager(); + var control = new MockFrameworkElement(); + + var regionScopeControl = new ContentControl(); + var accessor = new MockRegionManagerAccessor + { + GetRegionManager = d => d == regionScopeControl ? regionManager : null + }; + + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = new MockPresentationRegion() { Name = "myRegionName" }, + HostControl = control + }; + behavior.Attach(); + + Assert.False(regionManager.MockRegionCollection.AddCalled); + + regionScopeControl.Content = control; + accessor.UpdateRegions(); + + Assert.True(regionManager.MockRegionCollection.AddCalled); + regionManager.MockRegionCollection.AddCalled = false; + + accessor.UpdateRegions(); + Assert.False(regionManager.MockRegionCollection.AddCalled); + } + + [StaFact(Skip = "Review: Potentially not supported")] + public void RegionGetsRemovedFromRegionManagerWhenRemovedFromScope() + { + var regionManager = new MockRegionManager(); + var control = new MockFrameworkElement(); + var regionScopeControl = new ContentControl(); + var accessor = new MockRegionManagerAccessor + { + GetRegionManager = d => d == regionScopeControl ? regionManager : null + }; + + var region = new MockPresentationRegion() { Name = "myRegionName" }; + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = region, + HostControl = control + }; + behavior.Attach(); + + regionScopeControl.Content = control; + accessor.UpdateRegions(); + Assert.True(regionManager.MockRegionCollection.AddCalled); + Assert.Same(region, regionManager.MockRegionCollection.AddArgument); + + regionScopeControl.Content = null; + accessor.UpdateRegions(); + + Assert.True(regionManager.MockRegionCollection.RemoveCalled); + } + + [StaFact] + public void CanAttachBeforeSettingName() + { + var control = new ItemsControl(); + var regionManager = new MockRegionManager(); + var accessor = new MockRegionManagerAccessor + { + GetRegionManager = d => regionManager + }; + var region = new MockPresentationRegion() { Name = null }; + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = region, + HostControl = control + }; + + behavior.Attach(); + Assert.False(regionManager.MockRegionCollection.AddCalled); + + region.Name = "myRegionName"; + + Assert.True(regionManager.MockRegionCollection.AddCalled); + Assert.Same(region, regionManager.MockRegionCollection.AddArgument); + } + + [StaFact] + public void HostControlSetAfterAttachThrows() + { + var ex = Assert.Throws(() => + { + var behavior = new RegionManagerRegistrationBehavior(); + var hostControl1 = new MockDependencyObject(); + var hostControl2 = new MockDependencyObject(); + behavior.HostControl = hostControl1; + behavior.Attach(); + behavior.HostControl = hostControl2; + }); + + } + + [StaFact] + public async Task BehaviorDoesNotPreventRegionManagerFromBeingGarbageCollected() + { + var control = new MockFrameworkElement(); + var regionManager = new MockRegionManager(); + var regionManagerWeakReference = new WeakReference(regionManager); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName", + GetRegionManager = d => regionManager + }; + + var behavior = new RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = accessor, + Region = new MockPresentationRegion(), + HostControl = control + }; + + behavior.Attach(); + + Assert.True(regionManagerWeakReference.IsAlive); + GC.KeepAlive(regionManager); + + regionManager = null; + await Task.Delay(50); + + GC.Collect(); + + Assert.False(regionManagerWeakReference.IsAlive); + } + + internal class MockRegionManager : IRegionManager + { + public MockRegionCollection MockRegionCollection = new MockRegionCollection(); + + #region IRegionManager Members + + public IRegionCollection Regions + { + get { return MockRegionCollection; } + } + + IRegionManager IRegionManager.CreateRegionManager() + { + throw new System.NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, object view) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Type viewType) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + #endregion + + public bool Navigate(Uri source) + { + throw new NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + } + } + + internal class MockRegionCollection : IRegionCollection + { + public bool RemoveCalled; + public bool AddCalled; + public IRegion AddArgument; + + IEnumerator IEnumerable.GetEnumerator() + { + throw new System.NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new System.NotImplementedException(); + } + + public IRegion this[string regionName] + { + get { throw new System.NotImplementedException(); } + } + + public void Add(IRegion region) + { + AddCalled = true; + AddArgument = region; + } + + public bool Remove(string regionName) + { + RemoveCalled = true; + return true; + } + + public bool ContainsRegionWithName(string regionName) + { + throw new System.NotImplementedException(); + } + + public void Add(string regionName, IRegion region) + { + throw new NotImplementedException(); + } + + public event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged; + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionMemberLifetimeBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionMemberLifetimeBehaviorFixture.cs new file mode 100644 index 0000000000..495adc7710 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionMemberLifetimeBehaviorFixture.cs @@ -0,0 +1,261 @@ +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class RegionMemberLifetimeBehaviorFixture + { + protected Region Region { get; set; } = new(); + + protected RegionMemberLifetimeBehavior Behavior { get; set; } = new(); + + public RegionMemberLifetimeBehaviorFixture() + { + Arrange(); + } + + protected virtual void Arrange() + { + Region = new Region(); + Behavior = new RegionMemberLifetimeBehavior(); + Behavior.Region = Region; + Behavior.Attach(); + } + + [Fact] + public void WhenBehaviorAttachedThenReportsIsAttached() + { + Assert.True(Behavior.IsAttached); + } + + [Fact] + public void WhenIRegionMemberLifetimeItemReturnsKeepAliveFalseRemovesWhenInactive() + { + // Arrange + var regionItemMock = new Mock(); + regionItemMock.Setup(i => i.KeepAlive).Returns(false); + + Region.Add(regionItemMock.Object); + Region.Activate(regionItemMock.Object); + + // Act + Region.Deactivate(regionItemMock.Object); + + // Assert + Assert.False(Region.Views.Contains(regionItemMock.Object)); + } + + [Fact] + public void WhenIRegionMemberLifetimeItemReturnsKeepAliveTrueDoesNotRemoveOnDeactivation() + { + // Arrange + var regionItemMock = new Mock(); + regionItemMock.Setup(i => i.KeepAlive).Returns(true); + + Region.Add(regionItemMock.Object); + Region.Activate(regionItemMock.Object); + + // Act + Region.Deactivate(regionItemMock.Object); + + // Assert + Assert.True(Region.Views.Contains(regionItemMock.Object)); + + } + + [Fact] + public void WhenIRegionMemberLifetimeItemReturnsKeepAliveFalseCanRemoveFromRegion() + { + // Arrange + var regionItemMock = new Mock(); + regionItemMock.Setup(i => i.KeepAlive).Returns(false); + + var view = regionItemMock.Object; + + Region.Add(view); + Region.Activate(view); + + // The presence of the following two lines is essential for the test: + // we want to access both ActiveView and Views in that order + Assert.True(Region.ActiveViews.Contains(view)); + Assert.True(Region.Views.Contains(view)); + + // Act + // This may throw + Region.Remove(view); + + // Assert + Assert.False(Region.Views.Contains(view)); + Assert.False(Region.ActiveViews.Contains(view)); + } + + [Fact] + public void WhenRegionContainsMultipleMembers_OnlyRemovesThoseDeactivated() + { + // Arrange + var firstMockItem = new Mock(); + firstMockItem.Setup(i => i.KeepAlive).Returns(true); + + var secondMockItem = new Mock(); + secondMockItem.Setup(i => i.KeepAlive).Returns(false); + + Region.Add(firstMockItem.Object); + Region.Activate(firstMockItem.Object); + + Region.Add(secondMockItem.Object); + Region.Activate(secondMockItem.Object); + + // Act + Region.Deactivate(secondMockItem.Object); + + // Assert + Assert.True(Region.Views.Contains(firstMockItem.Object)); + Assert.False(Region.Views.Contains(secondMockItem.Object)); + } + + [Fact] + public void WhenMemberNeverActivatedThenIsNotRemovedOnAnothersDeactivation() + { + // Arrange + var firstMockItem = new Mock(); + firstMockItem.Setup(i => i.KeepAlive).Returns(false); + + var secondMockItem = new Mock(); + secondMockItem.Setup(i => i.KeepAlive).Returns(false); + + Region.Add(firstMockItem.Object); // Never activated + + Region.Add(secondMockItem.Object); + Region.Activate(secondMockItem.Object); + + // Act + Region.Deactivate(secondMockItem.Object); + + // Assert + Assert.True(Region.Views.Contains(firstMockItem.Object)); + Assert.False(Region.Views.Contains(secondMockItem.Object)); + } + + [StaFact] + public virtual void RemovesRegionItemIfDataContextReturnsKeepAliveFalse() + { + // Arrange + var regionItemMock = new Mock(); + regionItemMock.Setup(i => i.KeepAlive).Returns(false); + + var regionItem = new MockFrameworkElement(); + regionItem.DataContext = regionItemMock.Object; + + Region.Add(regionItem); + Region.Activate(regionItem); + + // Act + Region.Deactivate(regionItem); + + // Assert + Assert.False(Region.Views.Contains(regionItem)); + } + + [StaFact] + public virtual void RemovesOnlyDeactivatedItemsInRegionBasedOnDataContextKeepAlive() + { + // Arrange + var regionItemDataContextToKeepAlive = new Mock(); + regionItemDataContextToKeepAlive.Setup(i => i.KeepAlive).Returns(true); + + var regionItemToKeepAlive = new MockFrameworkElement(); + regionItemToKeepAlive.DataContext = regionItemDataContextToKeepAlive.Object; + Region.Add(regionItemToKeepAlive); + Region.Activate(regionItemToKeepAlive); + + var regionItemMock = new Mock(); + regionItemMock.Setup(i => i.KeepAlive).Returns(false); + + var regionItem = new MockFrameworkElement(); + regionItem.DataContext = regionItemMock.Object; + + Region.Add(regionItem); + Region.Activate(regionItem); + + // Act + Region.Deactivate(regionItem); + + // Assert + Assert.False(Region.Views.Contains(regionItem)); + Assert.True(Region.Views.Contains(regionItemToKeepAlive)); + } + + [Fact] + public virtual void WillRemoveDeactivatedItemIfKeepAliveAttributeFalse() + { + // Arrange + var regionItem = new RegionMemberNotKeptAlive(); + + Region.Add(regionItem); + Region.Activate(regionItem); + + // Act + Region.Deactivate(regionItem); + + // Assert + Assert.False(Region.Views.Contains((object)regionItem)); + } + + [Fact] + public virtual void WillNotRemoveDeactivatedItemIfKeepAliveAttributeTrue() + { + // Arrange + var regionItem = new RegionMemberKeptAlive(); + + Region.Add(regionItem); + Region.Activate(regionItem); + + // Act + Region.Deactivate(regionItem); + + // Assert + Assert.True(Region.Views.Contains((object)regionItem)); + } + + [StaFact] + public virtual void WillRemoveDeactivatedItemIfDataContextKeepAliveAttributeFalse() + { + // Arrange + var regionItemDataContext = new RegionMemberNotKeptAlive(); + var regionItem = new MockFrameworkElement() { DataContext = regionItemDataContext }; + Region.Add(regionItem); + Region.Activate(regionItem); + + // Act + Region.Deactivate(regionItem); + + // Assert + Assert.False(Region.Views.Contains(regionItem)); + } + + [RegionMemberLifetime(KeepAlive = false)] + public class RegionMemberNotKeptAlive + { + } + + [RegionMemberLifetime(KeepAlive = true)] + public class RegionMemberKeptAlive + { + } + } + + public class RegionMemberLifetimeBehaviorAgainstSingleActiveRegionFixture + : RegionMemberLifetimeBehaviorFixture + { + protected override void Arrange() + { + Region = new SingleActiveRegion(); + Behavior = new RegionMemberLifetimeBehavior(); + Behavior.Region = Region; + Behavior.Attach(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SelectorItemsSourceSyncRegionBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SelectorItemsSourceSyncRegionBehaviorFixture.cs new file mode 100644 index 0000000000..b24867f418 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SelectorItemsSourceSyncRegionBehaviorFixture.cs @@ -0,0 +1,226 @@ +// This feature is currently disabled +// See, Prism.Avalonia.Regions.Behaviors.SelectorItemsSourceSyncBehavior.cs for more info. +/* +using System; +using System.Collections; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Prism.Avalonia.Tests.Mocks; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class SelectorItemsSourceSyncRegionBehaviorFixture + { + [StaFact] + public void CanAttachToSelector() + { + SelectorItemsSourceSyncBehavior behavior = CreateBehavior(); + behavior.Attach(); + + Assert.True(behavior.IsAttached); + } + + [StaFact] + public void AttachSetsItemsSourceOfSelector() + { + SelectorItemsSourceSyncBehavior behavior = CreateBehavior(); + + var v1 = new Button(); + var v2 = new Button(); + + behavior.Region.Add(v1); + behavior.Region.Add(v2); + + behavior.Attach(); + + Assert.Equal(2, (behavior.HostControl as Selector).Items.Count); + } + + [StaFact] + public void IfViewsHaveSortHintThenViewsAreProperlySorted() + { + SelectorItemsSourceSyncBehavior behavior = CreateBehavior(); + + var v1 = new MockSortableView1(); + var v2 = new MockSortableView2(); + var v3 = new MockSortableView3(); + behavior.Attach(); + + behavior.Region.Add(v3); + behavior.Region.Add(v2); + behavior.Region.Add(v1); + + Assert.Equal(3, (behavior.HostControl as Selector).Items.Count); + + Assert.Same(v1, (behavior.HostControl as Selector).Items[0]); + Assert.Same(v2, (behavior.HostControl as Selector).Items[1]); + Assert.Same(v3, (behavior.HostControl as Selector).Items[2]); + } + + + [StaFact] + public void SelectionChangedShouldChangeActiveViews() + { + SelectorItemsSourceSyncBehavior behavior = CreateBehavior(); + + var v1 = new Button(); + var v2 = new Button(); + + behavior.Region.Add(v1); + behavior.Region.Add(v2); + + behavior.Attach(); + + (behavior.HostControl as Selector).SelectedItem = v1; + var activeViews = behavior.Region.ActiveViews; + + Assert.Single(activeViews); + Assert.Equal(v1, activeViews.First()); + + (behavior.HostControl as Selector).SelectedItem = v2; + + Assert.Single(activeViews); + Assert.Equal(v2, activeViews.First()); + } + + [StaFact] + public void ActiveViewChangedShouldChangeSelectedItem() + { + SelectorItemsSourceSyncBehavior behavior = CreateBehavior(); + + var v1 = new Button(); + var v2 = new Button(); + + behavior.Region.Add(v1); + behavior.Region.Add(v2); + + behavior.Attach(); + + behavior.Region.Activate(v1); + Assert.Equal(v1, (behavior.HostControl as Selector).SelectedItem); + + behavior.Region.Activate(v2); + Assert.Equal(v2, (behavior.HostControl as Selector).SelectedItem); + } + + [StaFact] + public void ItemsSourceSetThrows() + { + var ex = Assert.Throws(() => + { + SelectorItemsSourceSyncBehavior behavior = CreateBehavior(); + + (behavior.HostControl as Selector).ItemsSource = new[] { new Button() }; + + behavior.Attach(); + }); + + } + + [StaFact] + public void ControlWithExistingBindingOnItemsSourceWithNullValueThrows() + { + var behavor = CreateBehavior(); + + Binding binding = new Binding("Enumerable"); + binding.Source = new SimpleModel() { Enumerable = null }; + (behavor.HostControl as Selector).SetBinding(ItemsControl.ItemsSourceProperty, binding); + + try + { + behavor.Attach(); + + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Contains("ItemsControl's ItemsSource property is not empty.", ex.Message); + } + } + + [StaFact] + public void AddingViewToTwoRegionsThrows() + { + var ex = Assert.Throws(() => + { + var behavior1 = CreateBehavior(); + var behavior2 = CreateBehavior(); + + behavior1.Attach(); + behavior2.Attach(); + var v1 = new Button(); + + behavior1.Region.Add(v1); + behavior2.Region.Add(v1); + }); + + } + + [StaFact] + public void ReactivatingViewAddsViewToTab() + { + var behavior1 = CreateBehavior(); + behavior1.Attach(); + + var v1 = new Button(); + var v2 = new Button(); + + behavior1.Region.Add(v1); + behavior1.Region.Add(v2); + + behavior1.Region.Activate(v1); + Assert.True(behavior1.Region.ActiveViews.First() == v1); + + behavior1.Region.Activate(v2); + Assert.True(behavior1.Region.ActiveViews.First() == v2); + + behavior1.Region.Activate(v1); + Assert.True(behavior1.Region.ActiveViews.First() == v1); + } + + [StaFact] + public void ShouldAllowMultipleSelectedItemsForListBox() + { + var behavior1 = CreateBehavior(); + ListBox listBox = new ListBox(); + listBox.SelectionMode = SelectionMode.Multiple; + behavior1.HostControl = listBox; + behavior1.Attach(); + + var v1 = new Button(); + var v2 = new Button(); + + behavior1.Region.Add(v1); + behavior1.Region.Add(v2); + + listBox.SelectedItems.Add(v1); + listBox.SelectedItems.Add(v2); + + Assert.True(behavior1.Region.ActiveViews.Contains(v1)); + Assert.True(behavior1.Region.ActiveViews.Contains(v2)); + + } + + private SelectorItemsSourceSyncBehavior CreateBehavior() + { + Region region = new Region(); + Selector selector = new TabControl(); + + var behavior = new SelectorItemsSourceSyncBehavior(); + behavior.HostControl = selector; + behavior.Region = region; + return behavior; + } + + private class SimpleModel + { + public IEnumerable Enumerable { get; set; } + } + } +} +*/ diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SyncRegionContextWithHostBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SyncRegionContextWithHostBehaviorFixture.cs new file mode 100644 index 0000000000..006c6a3048 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SyncRegionContextWithHostBehaviorFixture.cs @@ -0,0 +1,141 @@ +using System; +using Avalonia; +using Prism.Avalonia.Tests.Mocks; +using Prism.Common; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class SyncRegionContextWithHostBehaviorFixture + { + [StaFact(Skip = "System.ArgumentNullException : Value cannot be null. (Parameter 'view')")] + public void ShouldForwardRegionContextValueToHostControl() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + AvaloniaObject mockDependencyObject = new MockDependencyObject(); + behavior.HostControl = mockDependencyObject; + + behavior.Attach(); + Assert.Null(region.Context); + RegionContext.GetObservableContext(mockDependencyObject).Value = "NewValue"; + + Assert.Equal("NewValue", region.Context); + + } + + [StaFact(Skip = "System.ArgumentNullException : Value cannot be null. (Parameter 'view')")] + public void ShouldUpdateHostControlRegionContextValueWhenContextOfRegionChanges() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + AvaloniaObject mockDependencyObject = new MockDependencyObject(); + behavior.HostControl = mockDependencyObject; + + ObservableObject observableRegionContext = RegionContext.GetObservableContext(mockDependencyObject); + + behavior.Attach(); + Assert.Null(observableRegionContext.Value); + region.Context = "NewValue"; + + Assert.Equal("NewValue", observableRegionContext.Value); + + } + + [StaFact(Skip = "System.ArgumentNullException : Value cannot be null. (Parameter 'view')")] + public void ShouldGetInitialValueFromHostAndSetOnRegion() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + AvaloniaObject mockDependencyObject = new MockDependencyObject(); + behavior.HostControl = mockDependencyObject; + + RegionContext.GetObservableContext(mockDependencyObject).Value = "NewValue"; + + Assert.Null(region.Context); + behavior.Attach(); + Assert.Equal("NewValue", region.Context); + + } + + [StaFact] + public void AttachShouldNotThrowWhenHostControlNull() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + behavior.Attach(); + } + + [StaFact] + public void AttachShouldNotThrowWhenHostControlNullAndRegionContextSet() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + behavior.Attach(); + region.Context = "Changed"; + } + + [StaFact(Skip = "System.ArgumentNullException : Value cannot be null. (Parameter 'view')")] + public void ChangingRegionContextObservableObjectValueShouldAlsoChangeRegionContextDependencyProperty() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + AvaloniaObject hostControl = new MockDependencyObject(); + behavior.HostControl = hostControl; + + behavior.Attach(); + + Assert.Null(RegionManager.GetRegionContext(hostControl)); + RegionContext.GetObservableContext(hostControl).Value = "NewValue"; + + Assert.Equal("NewValue", RegionManager.GetRegionContext(hostControl)); + } + + [StaFact(Skip = "Value cannot be null. (Parameter 'view')")] + public void AttachShouldChangeRegionContextDependencyProperty() + { + MockPresentationRegion region = new MockPresentationRegion(); + + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + behavior.Region = region; + AvaloniaObject hostControl = new MockDependencyObject(); + behavior.HostControl = hostControl; + + RegionContext.GetObservableContext(hostControl).Value = "NewValue"; + + Assert.Null(RegionManager.GetRegionContext(hostControl)); + behavior.Attach(); + Assert.Equal("NewValue", RegionManager.GetRegionContext(hostControl)); + } + + [StaFact] + public void SettingHostControlAfterAttachThrows() + { + var ex = Assert.Throws(() => + { + SyncRegionContextWithHostBehavior behavior = new SyncRegionContextWithHostBehavior(); + AvaloniaObject hostControl1 = new MockDependencyObject(); + behavior.HostControl = hostControl1; + + behavior.Attach(); + AvaloniaObject hostControl2 = new MockDependencyObject(); + behavior.HostControl = hostControl2; + }); + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/ContentControlRegionAdapterFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ContentControlRegionAdapterFixture.cs new file mode 100644 index 0000000000..d51cb994f1 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ContentControlRegionAdapterFixture.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Data; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class ContentControlRegionAdapterFixture + { + [StaFact] + public void AdapterAssociatesSelectorWithRegionActiveViews() + { + var control = new ContentControl(); + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + MockPresentationRegion region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + Assert.NotNull(region); + + Assert.Null(control.Content); + region.MockActiveViews.Items.Add(new object()); + + Assert.NotNull(control.Content); + Assert.Same(control.Content, region.ActiveViews.ElementAt(0)); + + region.MockActiveViews.Items.Add(new object()); + Assert.Same(control.Content, region.ActiveViews.ElementAt(0)); + + region.MockActiveViews.Items.RemoveAt(0); + Assert.Same(control.Content, region.ActiveViews.ElementAt(0)); + + region.MockActiveViews.Items.RemoveAt(0); + Assert.Null(control.Content); + } + + [StaFact] + public void ControlWithExistingContentThrows() + { + var control = new ContentControl() { Content = new object() }; + + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + try + { + var region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + //Assert.Fail(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Contains("ContentControl's Content property is not empty.", ex.Message); + } + } + + [StaFact] + public void ControlWithExistingBindingOnContentWithNullValueThrows() + { + var control = new ContentControl(); + Binding binding = new Binding("ObjectContents"); + binding.Source = new SimpleModel() { ObjectContents = null }; + control.SetValue(ContentControl.ContentProperty, binding); + /// WPF: control.SetBinding(ContentControl.ContentProperty, binding); + + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + try + { + var region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + //Assert.Fail(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Contains("ContentControl's Content property is not empty.", ex.Message); + } + } + + [StaFact] + public void AddedItemShouldBeActivated() + { + var control = new ContentControl(); + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + MockPresentationRegion region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + + var mockView = new object(); + region.Add(mockView); + + Assert.Single(region.ActiveViews); + Assert.True(region.ActiveViews.Contains(mockView)); + } + + [StaFact] + public void ShouldNotActivateAdditionalViewsAdded() + { + var control = new ContentControl(); + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + MockPresentationRegion region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + + var mockView = new object(); + region.Add(mockView); + region.Add(new object()); + + Assert.Single(region.ActiveViews); + Assert.True(region.ActiveViews.Contains(mockView)); + } + + [StaFact] + public void ShouldActivateAddedViewWhenNoneIsActive() + { + var control = new ContentControl(); + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + MockPresentationRegion region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + + var mockView1 = new object(); + region.Add(mockView1); + region.Deactivate(mockView1); + + var mockView2 = new object(); + region.Add(mockView2); + + Assert.Single(region.ActiveViews); + Assert.True(region.ActiveViews.Contains(mockView2)); + } + + [StaFact] + public void CanRemoveViewWhenNoneActive() + { + var control = new ContentControl(); + IRegionAdapter adapter = new TestableContentControlRegionAdapter(); + + MockPresentationRegion region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + + var mockView1 = new object(); + region.Add(mockView1); + region.Deactivate(mockView1); + region.Remove(mockView1); + Assert.Empty(region.ActiveViews); + } + + class SimpleModel + { + public Object ObjectContents { get; set; } + } + + private class TestableContentControlRegionAdapter : ContentControlRegionAdapter + { + public TestableContentControlRegionAdapter() : base(null) + { + } + + private MockPresentationRegion region = new MockPresentationRegion(); + + protected override IRegion CreateRegion() + { + return region; + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/ItemsControlRegionAdapterFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ItemsControlRegionAdapterFixture.cs new file mode 100644 index 0000000000..55b0ea0c99 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ItemsControlRegionAdapterFixture.cs @@ -0,0 +1,126 @@ +// TODO: 2022-07-12 +// REF: https://github.com/AvaloniaUI/Avalonia/issues/7553 +// Cannot perform the following. Check out, ContentControlRegionAdapterFixture.cs +// However, ItemsControl.Items is `IEnumerable` and doesn't play nicely. +// `control.Items.Add(view);` +// `control.Items[0]` +// Needs Tested: `control.SetBinding(ItemsControl.ItemsSourceProperty, binding);` +/* +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Prism.Navigation.Regions; +using Prism.Avalonia.Tests.Mocks; +using Avalonia.Controls; +using Avalonia.Data; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class ItemsControlRegionAdapterFixture + { + [StaFact] + public void AdapterAssociatesItemsControlWithRegion() + { + var control = new ItemsControl(); + IRegionAdapter adapter = new TestableItemsControlRegionAdapter(); + + IRegion region = adapter.Initialize(control, "Region1"); + Assert.NotNull(region); + + //// WPF: Assert.Same(control.ItemsSource, region.Views); + Assert.Same(control.Items, region.Views); + } + + [StaFact] + public void AdapterAssignsARegionThatHasAllViewsActive() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var control = new ItemsControl(); + IRegionAdapter adapter = new ItemsControlRegionAdapter(null); + + IRegion region = adapter.Initialize(control, "Region1"); + Assert.NotNull(region); + Assert.IsType(region); + } + + [StaFact] + public void ShouldMoveAlreadyExistingContentInControlToRegion() + { + var control = new ItemsControl(); + var view = new object(); + control.Items.Add(view); + IRegionAdapter adapter = new TestableItemsControlRegionAdapter(); + + var region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + + Assert.Single(region.MockViews); + Assert.Same(view, region.MockViews.ElementAt(0)); + Assert.Same(view, control.Items[0]); + } + + [StaFact] + public void ControlWithExistingItemSourceThrows() + { + var control = new ItemsControl() { Items = new List() }; + + IRegionAdapter adapter = new TestableItemsControlRegionAdapter(); + + try + { + var region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + //Assert.Fail(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Contains("ItemsControl's ItemsSource property is not empty.", ex.Message); + } + } + + [StaFact] + public void ControlWithExistingBindingOnItemsSourceWithNullValueThrows() + { + var control = new ItemsControl(); + Binding binding = new Binding("Enumerable"); + binding.Source = new SimpleModel() { Enumerable = null }; + // WPF: control.SetBinding(ItemsControl.ItemsSourceProperty, binding); + // NEEDS TESTED - (From, Suess): + control.SetValue(ItemsControl.ItemsProperty, binding); + + IRegionAdapter adapter = new TestableItemsControlRegionAdapter(); + + try + { + var region = (MockPresentationRegion)adapter.Initialize(control, "Region1"); + //Assert.Fail(); + } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Contains("ItemsControl's ItemsSource property is not empty.", ex.Message); + } + } + + class SimpleModel + { + public IEnumerable Enumerable { get; set; } + } + + private class TestableItemsControlRegionAdapter : ItemsControlRegionAdapter + { + public TestableItemsControlRegionAdapter() : base(null) + { + } + + private MockPresentationRegion region = new MockPresentationRegion(); + + protected override IRegion CreateRegion() + { + return region; + } + } + } +} +*/ diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/LocatorNavigationTargetHandlerFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/LocatorNavigationTargetHandlerFixture.cs new file mode 100644 index 0000000000..0ab86c9a45 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/LocatorNavigationTargetHandlerFixture.cs @@ -0,0 +1,345 @@ +using System; +using Avalonia.Controls; +using Moq; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Xunit; +using static Prism.Avalonia.Tests.Regions.LocatorNavigationTargetHandlerFixture; + +namespace Prism.Avalonia.Tests.Regions +{ + public class LocatorNavigationTargetHandlerFixture + { + [Fact] + public void WhenViewExistsAndDoesNotImplementINavigationAware_ThenReturnsView() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var view = new TestView(); + + region.Add(view); + + var navigationContext = new NavigationContext(null, new Uri(view.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(view, returnedView); + } + + [Fact] + public void WhenRegionHasMultipleViews_ThenViewsWithMatchingTypeNameAreConsidered() + { + // Arrange + + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var view1 = new TestView(); + var view2 = new Test2View(); + + region.Add(view1); + region.Add(view2); + var navigationContext = new NavigationContext(null, new Uri(view2.GetType().Name, UriKind.Relative)); + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + // Assert + Assert.Same(view2, returnedView); + } + + [Fact] + public void WhenRegionHasMultipleViews_ThenViewsWithMatchingFullTypeNameAreConsidered() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var view1 = new TestView(); + var view2 = new Test2View(); + + region.Add(view1); + region.Add(view2); + + var navigationContext = new NavigationContext(null, new Uri(view2.GetType().FullName, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(view2, returnedView); + } + + [Fact] + public void WhenViewExistsAndImplementsINavigationAware_ThenViewIsQueriedForNavigationAndIsReturnedIfAcceptsIt() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var viewMock = new Mock(); + viewMock + .Setup(v => v.IsNavigationTarget(It.IsAny())) + .Returns(true) + .Verifiable(); + + region.Add(viewMock.Object); + + var navigationContext = new NavigationContext(null, new Uri(viewMock.Object.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(viewMock.Object, returnedView); + viewMock.VerifyAll(); + } + + [StaFact] + public void WhenViewExistsAndHasDataContextThatImplementsINavigationAware_ThenDataContextIsQueriedForNavigationAndIsReturnedIfAcceptsIt() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var dataContextMock = new Mock(); + dataContextMock + .Setup(v => v.IsNavigationTarget(It.IsAny())) + .Returns(true) + .Verifiable(); + var viewMock = new Mock(); + viewMock.Object.DataContext = dataContextMock.Object; + + region.Add(viewMock.Object); + + var navigationContext = new NavigationContext(null, new Uri(viewMock.Object.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(viewMock.Object, returnedView); + dataContextMock.VerifyAll(); + } + + [Fact] + public void WhenNoCurrentMatchingViewExists_ThenReturnsNewlyCreatedInstanceWithServiceLocatorAddedToTheRegion() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var view = new TestView(); + + containerMock.Setup(sl => sl.Resolve(typeof(object), view.GetType().Name)).Returns(view); + + var navigationContext = new NavigationContext(null, new Uri(view.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(view, returnedView); + Assert.True(region.Views.Contains(view)); + } + + [Fact] + public void WhenViewExistsAndImplementsINavigationAware_ThenViewIsQueriedForNavigationAndNewInstanceIsCreatedIfItRejectsIt() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var viewMock = new Mock(); + viewMock + .Setup(v => v.IsNavigationTarget(It.IsAny())) + .Returns(false) + .Verifiable(); + + region.Add(viewMock.Object); + + var newView = new TestView(); + + containerMock.Setup(sl => sl.Resolve(typeof(object), viewMock.Object.GetType().Name)).Returns(newView); + + var navigationContext = new NavigationContext(null, new Uri(viewMock.Object.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(newView, returnedView); + Assert.True(region.Views.Contains(newView)); + viewMock.VerifyAll(); + } + + [StaFact] + public void WhenViewExistsAndHasDataContextThatImplementsINavigationAware_ThenDataContextIsQueriedForNavigationAndNewInstanceIsCreatedIfItRejectsIt() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var dataContextMock = new Mock(); + dataContextMock + .Setup(v => v.IsNavigationTarget(It.IsAny())) + .Returns(false) + .Verifiable(); + + var viewMock = new Mock(); + viewMock.Object.DataContext = dataContextMock.Object; + + region.Add(viewMock.Object); + + var newView = new TestView(); + + containerMock.Setup(sl => sl.Resolve(typeof(object), viewMock.Object.GetType().Name)).Returns(newView); + + var navigationContext = new NavigationContext(null, new Uri(viewMock.Object.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var returnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(newView, returnedView); + Assert.True(region.Views.Contains(newView)); + dataContextMock.VerifyAll(); + } + + [Fact] + public void WhenViewCannotBeCreated_ThenThrowsAnException() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + containerMock.Setup(sl => sl.Resolve(typeof(object), typeof(TestView).Name)).Throws(); + + var region = new Region(); + + var navigationContext = new NavigationContext(null, new Uri(typeof(TestView).Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + ExceptionAssert.Throws( + () => + { + navigationTargetHandler.LoadContent(region, navigationContext); + + }); + } + + [Fact] + public void WhenViewAddedByHandlerDoesNotImplementINavigationAware_ThenReturnsView() + { + // Arrange + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var view = new TestView(); + + containerMock.Setup(sl => sl.Resolve(typeof(object), view.GetType().Name)).Returns(view); + + var navigationContext = new NavigationContext(null, new Uri(view.GetType().Name, UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + // Act + var firstReturnedView = navigationTargetHandler.LoadContent(region, navigationContext); + var secondReturnedView = navigationTargetHandler.LoadContent(region, navigationContext); + + // Assert + Assert.Same(view, firstReturnedView); + Assert.Same(view, secondReturnedView); + containerMock.Verify(sl => sl.Resolve(typeof(object), view.GetType().Name), Times.Once()); + } + + [Fact] + public void WhenRequestingContentForNullRegion_ThenThrows() + { + var containerMock = new Mock(); + + var navigationContext = new NavigationContext(null, new Uri("/", UriKind.Relative)); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + ExceptionAssert.Throws( + () => + { + navigationTargetHandler.LoadContent(null, navigationContext); + + }); + } + + [Fact] + public void WhenRequestingContentForNullContext_ThenThrows() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var region = new Region(); + + var navigationTargetHandler = new TestRegionNavigationContentLoader(containerMock.Object); + + ExceptionAssert.Throws( + () => + { + navigationTargetHandler.LoadContent(region, null); + + }); + } + + public class TestRegionNavigationContentLoader : RegionNavigationContentLoader + { + public TestRegionNavigationContentLoader(IContainerExtension container) + : base(container) + { } + } + + public class TestView { } + + public class Test2View { } + } + + public class ActivationException : Exception + { + public ActivationException() + { + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationAsyncExtensionsFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationAsyncExtensionsFixture.cs new file mode 100644 index 0000000000..74a6a374f0 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationAsyncExtensionsFixture.cs @@ -0,0 +1,107 @@ +using System; +using Moq; +using Prism.Navigation; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class NavigationAsyncExtensionsFixture + { + [Fact] + public void WhenNavigatingWithANullThis_ThenThrows() + { + INavigateAsync navigate = null; + string target = ""; + + ExceptionAssert.Throws( + () => + { + navigate.RequestNavigate(target); + }); + } + + [Fact] + public void WhenNavigatingWithANullStringTarget_ThenThrows() + { + INavigateAsync navigate = new Mock().Object; + string target = null; + + ExceptionAssert.Throws( + () => + { + navigate.RequestNavigate(target); + }); + } + + [Fact] + public void WhenNavigatingWithARelativeStringTarget_ThenNavigatesToRelativeUri() + { + var navigateMock = new Mock(); + navigateMock + .Setup(nv => + nv.RequestNavigate( + It.Is(u => !u.IsAbsoluteUri && u.OriginalString == "relative"), + It.Is>(c => c != null), + It.IsAny())) + .Verifiable(); + + string target = "relative"; + + navigateMock.Object.RequestNavigate(target); + + navigateMock.VerifyAll(); + } + + [Fact] + public void WhenNavigatingWithAnAbsoluteStringTarget_ThenNavigatesToAbsoluteUri() + { + var navigateMock = new Mock(); + navigateMock + .Setup(nv => + nv.RequestNavigate( + It.Is(u => u.IsAbsoluteUri && u.Host == "test" && u.AbsolutePath == "/path"), + It.Is>(c => c != null), + It.IsAny())) + .Verifiable(); + + string target = "http://test/path"; + + navigateMock.Object.RequestNavigate(target); + + navigateMock.VerifyAll(); + } + + [Fact] + public void WhenNavigatingWithANullThisAndAUri_ThenThrows() + { + INavigateAsync navigate = null; + Uri target = new Uri("test", UriKind.Relative); + + ExceptionAssert.Throws( + () => + { + navigate.RequestNavigate(target); + }); + } + + [Fact] + public void WhenNavigatingWithAUri_ThenNavigatesToUriWithCallback() + { + Uri target = new Uri("relative", UriKind.Relative); + + var navigateMock = new Mock(); + navigateMock + .Setup(nv => + nv.RequestNavigate( + target, + It.Is>(c => c != null), + It.IsAny())) + .Verifiable(); + + navigateMock.Object.RequestNavigate(target); + + navigateMock.VerifyAll(); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationContextFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationContextFixture.cs new file mode 100644 index 0000000000..ba68fe1a19 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationContextFixture.cs @@ -0,0 +1,47 @@ +using System; +using Moq; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class NavigationContextFixture + { + [Fact] + public void WhenCreatingANewContextForAUriWithAQuery_ThenNewContextInitializesPropertiesAndExtractsTheQuery() + { + var uri = new Uri("test?name=value", UriKind.Relative); + + var navigationJournalMock = new Mock(); + var navigationServiceMock = new Mock(); + + IRegion region = new Region(); + navigationServiceMock.SetupGet(n => n.Region).Returns(region); + navigationServiceMock.SetupGet(x => x.Journal).Returns(navigationJournalMock.Object); + + var context = new NavigationContext(navigationServiceMock.Object, uri); + + Assert.Same(navigationServiceMock.Object, context.NavigationService); + Assert.Equal(uri, context.Uri); + Assert.Single(context.Parameters); + Assert.Equal("value", context.Parameters["name"]); + } + + [Fact] + public void WhenCreatingANewContextForAUriWithNoQuery_ThenNewContextInitializesPropertiesGetsEmptyQuery() + { + var uri = new Uri("test", UriKind.Relative); + + var navigationJournalMock = new Mock(); + + var navigationServiceMock = new Mock(); + navigationServiceMock.SetupGet(x => x.Journal).Returns(navigationJournalMock.Object); + + var context = new NavigationContext(navigationServiceMock.Object, uri); + + Assert.Same(navigationServiceMock.Object, context.NavigationService); + Assert.Equal(uri, context.Uri); + Assert.Empty(context.Parameters); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterBaseFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterBaseFixture.cs new file mode 100644 index 0000000000..32516b5c01 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterBaseFixture.cs @@ -0,0 +1,110 @@ +using System; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionAdapterBaseFixture + { + [Fact] + public void IncorrectTypeThrows() + { + var ex = Assert.Throws(() => + { + IRegionAdapter adapter = new TestableRegionAdapterBase(); + adapter.Initialize(new MockDependencyObject(), "Region1"); + }); + } + + [Fact] + public void InitializeSetsRegionName() + { + IRegionAdapter adapter = new TestableRegionAdapterBase(); + var region = adapter.Initialize(new MockRegionTarget(), "Region1"); + Assert.Equal("Region1", region.Name); + } + + [Fact] + public void NullRegionNameThrows() + { + var ex = Assert.Throws(() => + { + IRegionAdapter adapter = new TestableRegionAdapterBase(); + var region = adapter.Initialize(new MockRegionTarget(), null); + }); + + } + + [Fact] + public void NullObjectThrows() + { + var ex = Assert.Throws(() => + { + IRegionAdapter adapter = new TestableRegionAdapterBase(); + adapter.Initialize(null, "Region1"); + }); + + } + + [Fact] + public void CreateRegionReturnValueIsPassedToAdapt() + { + var regionTarget = new MockRegionTarget(); + var adapter = new TestableRegionAdapterBase(); + + adapter.Initialize(regionTarget, "Region1"); + + Assert.Same(adapter.CreateRegionReturnValue, adapter.AdaptArgumentRegion); + Assert.Same(regionTarget, adapter.adaptArgumentRegionTarget); + } + + [Fact] + public void CreateRegionReturnValueIsPassedToAttachBehaviors() + { + var regionTarget = new MockRegionTarget(); + var adapter = new TestableRegionAdapterBase(); + + var region = adapter.Initialize(regionTarget, "Region1"); + + Assert.Same(adapter.CreateRegionReturnValue, adapter.AttachBehaviorsArgumentRegion); + Assert.Same(regionTarget, adapter.attachBehaviorsArgumentTargetToAdapt); + } + + class TestableRegionAdapterBase : RegionAdapterBase + { + public IRegion CreateRegionReturnValue = new MockPresentationRegion(); + public IRegion AdaptArgumentRegion; + public MockRegionTarget adaptArgumentRegionTarget; + public IRegion AttachBehaviorsArgumentRegion; + public MockRegionTarget attachBehaviorsArgumentTargetToAdapt; + + public TestableRegionAdapterBase() : base(null) + { + + } + + protected override void Adapt(IRegion region, MockRegionTarget regionTarget) + { + AdaptArgumentRegion = region; + adaptArgumentRegionTarget = regionTarget; + } + + protected override IRegion CreateRegion() + { + return CreateRegionReturnValue; + } + + protected override void AttachBehaviors(IRegion region, MockRegionTarget regionTarget) + { + AttachBehaviorsArgumentRegion = region; + attachBehaviorsArgumentTargetToAdapt = regionTarget; + base.AttachBehaviors(region, regionTarget); + } + } + + class MockRegionTarget + { + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterMappingsFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterMappingsFixture.cs new file mode 100644 index 0000000000..30f433ecdb --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterMappingsFixture.cs @@ -0,0 +1,148 @@ +using Prism.Navigation.Regions; +using Prism.Avalonia.Tests.Mocks; +using Avalonia.Controls; +using Moq; +using Prism.Ioc; +using Xunit; +using System; +using System.Collections.Generic; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionAdapterMappingsFixture + { + [Fact] + public void ShouldGetRegisteredMapping() + { + var regionAdapterMappings = new RegionAdapterMappings(); + Type registeredType = typeof(ItemsControl); + var regionAdapter = new MockRegionAdapter(); + + regionAdapterMappings.RegisterMapping(registeredType, regionAdapter); + var returnedAdapter = regionAdapterMappings.GetMapping(registeredType); + + Assert.NotNull(returnedAdapter); + Assert.Same(regionAdapter, returnedAdapter); + } + + [Fact] + public void ShouldGetRegisteredMapping_UsingGenericControl() + { + var regionAdapterMappings = new RegionAdapterMappings(); + var regionAdapter = new MockRegionAdapter(); + + regionAdapterMappings.RegisterMapping(regionAdapter); + + var returnedAdapter = regionAdapterMappings.GetMapping(); + + Assert.NotNull(returnedAdapter); + Assert.Same(regionAdapter, returnedAdapter); + } + + [Fact] + public void ShouldGetRegisteredMapping_UsingGenericControlAndAdapter() + { + try + { + var regionAdapterMappings = new RegionAdapterMappings(); + var regionAdapter = new MockRegionAdapter(); + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(MockRegionAdapter))) + .Returns(regionAdapter); + ContainerLocator.ResetContainer(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + regionAdapterMappings.RegisterMapping(); + + var returnedAdapter = regionAdapterMappings.GetMapping(); + + Assert.NotNull(returnedAdapter); + Assert.Same(regionAdapter, returnedAdapter); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void ShouldGetMappingForDerivedTypesThanTheRegisteredOnes() + { + var regionAdapterMappings = new RegionAdapterMappings(); + var regionAdapter = new MockRegionAdapter(); + + regionAdapterMappings.RegisterMapping(typeof(ItemsControl), regionAdapter); + var returnedAdapter = regionAdapterMappings.GetMapping(typeof(ItemsControlDescendant)); + + Assert.NotNull(returnedAdapter); + Assert.Same(regionAdapter, returnedAdapter); + } + + [Fact] + public void GetMappingOfUnregisteredTypeThrows() + { + var ex = Assert.Throws(() => + { + var regionAdapterMappings = new RegionAdapterMappings(); + regionAdapterMappings.GetMapping(typeof(object)); + }); + + } + + [Fact] + public void ShouldGetTheMostSpecializedMapping() + { + var regionAdapterMappings = new RegionAdapterMappings(); + var genericAdapter = new MockRegionAdapter(); + var specializedAdapter = new MockRegionAdapter(); + + regionAdapterMappings.RegisterMapping(typeof(ItemsControl), genericAdapter); + regionAdapterMappings.RegisterMapping(typeof(ItemsControlDescendant), specializedAdapter); + var returnedAdapter = regionAdapterMappings.GetMapping(typeof(ItemsControlDescendant)); + + Assert.NotNull(returnedAdapter); + Assert.Same(specializedAdapter, returnedAdapter); + } + + [Fact] + public void RegisterAMappingThatAlreadyExistsThrows() + { + var ex = Assert.Throws(() => + { + var regionAdapterMappings = new RegionAdapterMappings(); + var regionAdapter = new MockRegionAdapter(); + + regionAdapterMappings.RegisterMapping(typeof(ItemsControl), regionAdapter); + regionAdapterMappings.RegisterMapping(typeof(ItemsControl), regionAdapter); + }); + } + + [Fact] + public void NullControlThrows() + { + var ex = Assert.Throws(() => + { + var regionAdapterMappings = new RegionAdapterMappings(); + var regionAdapter = new MockRegionAdapter(); + + regionAdapterMappings.RegisterMapping(null, regionAdapter); + }); + + } + + [Fact] + public void NullAdapterThrows() + { + var ex = Assert.Throws(() => + { + var regionAdapterMappings = new RegionAdapterMappings(); + + regionAdapterMappings.RegisterMapping(typeof(ItemsControl), null); + }); + + } + + class ItemsControlDescendant : ItemsControl { } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorCollectionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorCollectionFixture.cs new file mode 100644 index 0000000000..0e6620159d --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorCollectionFixture.cs @@ -0,0 +1,41 @@ +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionBehaviorCollectionFixture + { + [Fact] + public void CanAttachRegionBehaviors() + { + RegionBehaviorCollection behaviorCollection = new RegionBehaviorCollection(new MockPresentationRegion()); + + var mock1 = new MockRegionBehavior(); + bool mock1Attached = false; + mock1.OnAttach = () => mock1Attached = true; + behaviorCollection.Add("Mock1", mock1); + + var mock2 = new MockRegionBehavior(); + bool mock2Attached = false; + mock2.OnAttach = () => mock2Attached = true; + behaviorCollection.Add("Mock2", mock2); + + Assert.True(mock1Attached); + Assert.True(mock2Attached); + } + + [Fact] + public void ShouldAddRegionWhenAddingBehavior() + { + var region = new MockPresentationRegion(); + RegionBehaviorCollection behaviorCollection = new RegionBehaviorCollection(region); + var behavior = new MockRegionBehavior(); + + behaviorCollection.Add("Mock", behavior); + + Assert.NotNull(behavior.Region); + Assert.Same(region, behavior.Region); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFactoryFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFactoryFixture.cs new file mode 100644 index 0000000000..9f49b53dbe --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFactoryFixture.cs @@ -0,0 +1,75 @@ +using Prism.Avalonia.Tests.Mocks; +using Moq; +using Prism.Ioc; +using Xunit; +using Prism.Navigation.Regions; +using System.Linq; +using System; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionBehaviorFactoryFixture + { + [Fact] + public void CanRegisterType() + { + RegionBehaviorFactory factory = new RegionBehaviorFactory(null); + + factory.AddIfMissing("key1", typeof(MockRegionBehavior)); + factory.AddIfMissing("key2", typeof(MockRegionBehavior)); + + Assert.Equal(2, factory.Count()); + Assert.True(factory.ContainsKey("key1")); + + } + + [Fact] + public void WillNotAddTypesWithDuplicateKeys() + { + RegionBehaviorFactory factory = new RegionBehaviorFactory(null); + + factory.AddIfMissing("key1", typeof(MockRegionBehavior)); + factory.AddIfMissing("key1", typeof(MockRegionBehavior)); + + Assert.Single(factory); + } + + [Fact] + public void AddTypeThatDoesNotInheritFromIRegionBehaviorThrows() + { + var ex = Assert.Throws(() => + { + RegionBehaviorFactory factory = new RegionBehaviorFactory(null); + + factory.AddIfMissing("key1", typeof(object)); + }); + + } + + [Fact] + public void CanCreateRegisteredType() + { + var expectedBehavior = new MockRegionBehavior(); + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(MockRegionBehavior))).Returns(expectedBehavior); + RegionBehaviorFactory factory = new RegionBehaviorFactory(containerMock.Object); + + factory.AddIfMissing("key1", typeof(MockRegionBehavior)); + var behavior = factory.CreateFromKey("key1"); + + Assert.Same(expectedBehavior, behavior); + } + + [Fact] + public void CreateWithUnknownKeyThrows() + { + var ex = Assert.Throws(() => + { + RegionBehaviorFactory factory = new RegionBehaviorFactory(null); + + factory.CreateFromKey("Key1"); + }); + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFixture.cs new file mode 100644 index 0000000000..8b605d4f56 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFixture.cs @@ -0,0 +1,58 @@ +using System; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionBehaviorFixture + { + [Fact] + public void CannotChangeRegionAfterAttach() + { + var ex = Assert.Throws(() => + { + TestableRegionBehavior regionBehavior = new TestableRegionBehavior(); + + regionBehavior.Region = new MockPresentationRegion(); + + regionBehavior.Attach(); + regionBehavior.Region = new MockPresentationRegion(); + }); + + } + + [Fact] + public void ShouldFailWhenAttachedWithoutRegion() + { + var ex = Assert.Throws(() => + { + TestableRegionBehavior regionBehavior = new TestableRegionBehavior(); + regionBehavior.Attach(); + }); + + } + + [Fact] + public void ShouldCallOnAttachWhenAttachMethodIsInvoked() + { + TestableRegionBehavior regionBehavior = new TestableRegionBehavior(); + + regionBehavior.Region = new MockPresentationRegion(); + + regionBehavior.Attach(); + + Assert.True(regionBehavior.onAttachCalled); + } + + private class TestableRegionBehavior : RegionBehavior + { + public bool onAttachCalled; + + protected override void OnAttach() + { + onAttachCalled = true; + } + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionFixture.cs new file mode 100644 index 0000000000..2dda17d714 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionFixture.cs @@ -0,0 +1,649 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using Moq; +using Prism.Ioc; +using Prism.Avalonia.Tests.Mocks; +using Xunit; +using Prism.Navigation.Regions; +using Prism.Navigation; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionFixture + { + [Fact] + public void WhenRegionConstructed_SortComparisonIsDefault() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + + Assert.NotNull(region.SortComparison); + Assert.Equal(region.SortComparison, Region.DefaultSortComparison); + } + + [Fact] + public void CanAddContentToRegion() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + + Assert.Empty(region.Views.Cast()); + + region.Add(new object()); + + Assert.Single(region.Views.Cast()); + } + + [Fact] + public void CanRemoveContentFromRegion() + { + IRegion region = new Region(); + object view = new object(); + + region.Add(view); + region.Remove(view); + + Assert.Empty(region.Views.Cast()); + } + + [Fact] + public void RemoveInexistentViewThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + object view = new object(); + + region.Remove(view); + + Assert.Empty(region.Views.Cast()); + }); + + } + + [Fact] + public void RegionExposesCollectionOfContainedViews() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + + object view = new object(); + + region.Add(view); + + var views = region.Views; + + Assert.NotNull(views); + Assert.Single(views.Cast()); + Assert.Same(view, views.Cast().ElementAt(0)); + } + + [Fact] + public void CanAddAndRetrieveNamedViewInstance() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + object myView = new object(); + region.Add(myView, "MyView"); + object returnedView = region.GetView("MyView"); + + Assert.NotNull(returnedView); + Assert.Same(returnedView, myView); + } + + [Fact] + public void AddingDuplicateNamedViewThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + + region.Add(new object(), "MyView"); + region.Add(new object(), "MyView"); + }); + + } + + [Fact] + public void AddNamedViewIsAlsoListedInViewsCollection() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + object myView = new object(); + + region.Add(myView, "MyView"); + + Assert.Single(region.Views.Cast()); + Assert.Same(myView, region.Views.Cast().ElementAt(0)); + } + + [Fact] + public void GetViewReturnsNullWhenViewDoesNotExistInRegion() + { + IRegion region = new Region(); + + Assert.Null(region.GetView("InexistentView")); + } + + [Fact] + public void GetViewWithNullOrEmptyStringThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + + region.GetView(string.Empty); + }); + + } + + [Fact] + public void AddNamedViewWithNullOrEmptyStringNameThrows() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + + region.Add(new object(), string.Empty); + }); + + } + + [Fact] + public void GetViewReturnsNullAfterRemovingViewFromRegion() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + object myView = new object(); + region.Add(myView, "MyView"); + region.Remove(myView); + + Assert.Null(region.GetView("MyView")); + } + + [Fact] + public void AddViewPassesSameScopeByDefaultToView() + { + var regionManager = new MockRegionManager(); + IRegion region = new Region(); + region.RegionManager = regionManager; + var myView = new MockDependencyObject(); + + region.Add(myView); + + Assert.Same(regionManager, myView.GetValue(RegionManager.RegionManagerProperty)); + } + + [Fact] + public void AddViewPassesSameScopeByDefaultToNamedView() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new MockRegionManager(); + IRegion region = new Region(); + region.RegionManager = regionManager; + var myView = new MockDependencyObject(); + + region.Add(myView, "MyView"); + + Assert.Same(regionManager, myView.GetValue(RegionManager.RegionManagerProperty)); + } + + [Fact] + public void AddViewPassesDifferentScopeWhenAdding() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new MockRegionManager(); + IRegion region = new Region(); + region.RegionManager = regionManager; + var myView = new MockDependencyObject(); + + region.Add(myView, "MyView", true); + + Assert.NotSame(regionManager, myView.GetValue(RegionManager.RegionManagerProperty)); + } + + [Fact] + public void CreatingNewScopesAsksTheRegionManagerForNewInstance() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new MockRegionManager(); + IRegion region = new Region(); + region.RegionManager = regionManager; + var myView = new object(); + + region.Add(myView, "MyView", true); + + Assert.True(regionManager.CreateRegionManagerCalled); + } + + [Fact] + public void AddViewReturnsExistingRegionManager() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new MockRegionManager(); + IRegion region = new Region(); + region.RegionManager = regionManager; + var myView = new object(); + + var returnedRegionManager = region.Add(myView, "MyView", false); + + Assert.Same(regionManager, returnedRegionManager); + } + + [Fact] + public void AddViewReturnsNewRegionManager() + { + var regionManager = new MockRegionManager(); + IRegion region = new Region(); + region.RegionManager = regionManager; + var myView = new object(); + + var returnedRegionManager = region.Add(myView, "MyView", true); + + Assert.NotSame(regionManager, returnedRegionManager); + } + + [Fact] + public void AddingNonDependencyObjectToRegionDoesNotThrow() + { + IRegion region = new Region(); + object model = new object(); + + region.Add(model); + + Assert.Single(region.Views.Cast()); + } + + [Fact] + public void ActivateNonAddedViewThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + + object nonAddedView = new object(); + + region.Activate(nonAddedView); + }); + + } + + [Fact] + public void DeactivateNonAddedViewThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + + object nonAddedView = new object(); + + region.Deactivate(nonAddedView); + }); + + } + + [Fact] + public void ActivateNullViewThrows() + { + var ex = Assert.Throws(() => + { + IRegion region = new Region(); + + region.Activate(null); + }); + + } + + [Fact] + public void AddViewRaisesCollectionViewEvent() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + bool viewAddedCalled = false; + + IRegion region = new Region(); + region.Views.CollectionChanged += (sender, e) => + { + if (e.Action == NotifyCollectionChangedAction.Add) + viewAddedCalled = true; + }; + + object model = new object(); + Assert.False(viewAddedCalled); + region.Add(model); + + Assert.True(viewAddedCalled); + } + + [Fact] + public void ViewAddedEventPassesTheViewAddedInTheEventArgs() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + object viewAdded = null; + + IRegion region = new Region(); + region.Views.CollectionChanged += (sender, e) => + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + viewAdded = e.NewItems[0]; + } + }; + object model = new object(); + Assert.Null(viewAdded); + region.Add(model); + + Assert.NotNull(viewAdded); + Assert.Same(model, viewAdded); + } + + [Fact] + public void RemoveViewFiresViewRemovedEvent() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + bool viewRemoved = false; + + IRegion region = new Region(); + object model = new object(); + region.Views.CollectionChanged += (sender, e) => + { + if (e.Action == NotifyCollectionChangedAction.Remove) + viewRemoved = true; + }; + + region.Add(model); + Assert.False(viewRemoved); + + region.Remove(model); + + Assert.True(viewRemoved); + } + + [Fact] + public void ViewRemovedEventPassesTheViewRemovedInTheEventArgs() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + object removedView = null; + + IRegion region = new Region(); + region.Views.CollectionChanged += (sender, e) => + { + if (e.Action == NotifyCollectionChangedAction.Remove) + removedView = e.OldItems[0]; + }; + object model = new object(); + region.Add(model); + Assert.Null(removedView); + + region.Remove(model); + + Assert.Same(model, removedView); + } + + [Fact] + public void ShowViewFiresViewShowedEvent() + { + bool viewActivated = false; + + IRegion region = new Region(); + object model = new object(); + region.ActiveViews.CollectionChanged += (o, e) => + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.Contains(model)) + viewActivated = true; + }; + region.Add(model); + Assert.False(viewActivated); + + region.Activate(model); + + Assert.True(viewActivated); + } + + [Fact] + public void AddingSameViewTwiceThrows() + { + object view = new object(); + IRegion region = new Region(); + region.Add(view); + + try + { + region.Add(view); + //Assert.Fail(); + } + catch (InvalidOperationException ex) + { + Assert.Equal("View already exists in region.", ex.Message); + } + catch + { + //Assert.Fail(); + } + } + + [Fact] + public void RemovingViewAlsoRemovesItFromActiveViews() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + object model = new object(); + region.Add(model); + region.Activate(model); + Assert.True(region.ActiveViews.Contains(model)); + + region.Remove(model); + + Assert.False(region.ActiveViews.Contains(model)); + } + + [Fact] + public void ShouldGetNotificationWhenContextChanges() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + bool contextChanged = false; + region.PropertyChanged += (s, args) => { if (args.PropertyName == "Context") contextChanged = true; }; + + region.Context = "MyNewContext"; + + Assert.True(contextChanged); + } + + [Fact] + public void ChangingNameOnceItIsSetThrows() + { + var ex = Assert.Throws(() => + { + var region = new Region + { + Name = "MyRegion" + }; + + region.Name = "ChangedRegionName"; + }); + + } + + private class MockRegionManager : IRegionManager + { + public bool CreateRegionManagerCalled; + + public IRegionManager CreateRegionManager() + { + CreateRegionManagerCalled = true; + return new MockRegionManager(); + } + + public IRegionManager AddToRegion(string regionName, object view) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Type viewType) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public IRegionCollection Regions + { + get { throw new NotImplementedException(); } + } + + public IRegion AttachNewRegion(object regionTarget, string regionName) + { + throw new NotImplementedException(); + } + + public bool Navigate(Uri source) + { + throw new NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + } + + [Fact] + public void NavigateDelegatesToIRegionNavigationService() + { + try + { + // Prepare + IRegion region = new Region(); + + object view = new object(); + region.Add(view); + + Uri uri = new Uri(view.GetType().Name, UriKind.Relative); + Action navigationCallback = nr => { }; + NavigationParameters navigationParameters = new NavigationParameters(); + + Mock mockRegionNavigationService = new Mock(); + mockRegionNavigationService.Setup(x => x.RequestNavigate(uri, navigationCallback, navigationParameters)).Verifiable(); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationService))).Returns(mockRegionNavigationService.Object); + ContainerLocator.ResetContainer(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + // Act + region.RequestNavigate(uri, navigationCallback, navigationParameters); + + // Verify + mockRegionNavigationService.VerifyAll(); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void WhenViewsWithSortHintsAdded_RegionSortsViews() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region(); + + object view1 = new ViewOrder1(); + object view2 = new ViewOrder2(); + object view3 = new ViewOrder3(); + + region.Add(view1); + region.Add(view2); + region.Add(view3); + + Assert.Equal(3, region.Views.Count()); + Assert.Same(view2, region.Views.ElementAt(0)); + Assert.Same(view3, region.Views.ElementAt(1)); + Assert.Same(view1, region.Views.ElementAt(2)); + } + + [StaFact] + public void WhenViewHasBeenRemovedAndRegionManagerPropertyCleared_ThenItCanBeAddedAgainToARegion() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new Region { RegionManager = new MockRegionManager() }; + + var view = new MockFrameworkElement(); + + var scopedRegionManager = region.Add(view, null, true); + + Assert.Equal(view, region.Views.First()); + + region.Remove(view); + + view.ClearValue(RegionManager.RegionManagerProperty); + + Assert.Empty(region.Views.Cast()); + + var newScopedRegion = region.Add(view, null, true); + + Assert.Equal(view, region.Views.First()); + + Assert.Same(newScopedRegion, view.GetValue(RegionManager.RegionManagerProperty)); + } + + [ViewSortHint("C")] + private class ViewOrder1 { }; + + [ViewSortHint("A")] + private class ViewOrder2 { }; + + [ViewSortHint("B")] + private class ViewOrder3 { }; + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs new file mode 100644 index 0000000000..255ecfc60a --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs @@ -0,0 +1,501 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Threading.Tasks; +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionManagerFixture + { + [Fact] + public void CanAddRegion() + { + IRegion region1 = new MockPresentationRegion(); + region1.Name = "MainRegion"; + + RegionManager regionManager = new RegionManager(); + regionManager.Regions.Add(region1); + + IRegion region2 = regionManager.Regions["MainRegion"]; + Assert.Same(region1, region2); + } + + [Fact] + public void ShouldFailIfRegionDoesntExists() + { + var ex = Assert.Throws(() => + { + RegionManager regionManager = new RegionManager(); + IRegion region = regionManager.Regions["nonExistentRegion"]; + }); + } + + [Fact] + public void CanCheckTheExistenceOfARegion() + { + RegionManager regionManager = new RegionManager(); + bool result = regionManager.Regions.ContainsRegionWithName("noRegion"); + + Assert.False(result); + + IRegion region = new MockPresentationRegion(); + region.Name = "noRegion"; + regionManager.Regions.Add(region); + + result = regionManager.Regions.ContainsRegionWithName("noRegion"); + + Assert.True(result); + } + + [Fact] + public void AddingMultipleRegionsWithSameNameThrowsArgumentException() + { + var ex = Assert.Throws(() => + { + var regionManager = new RegionManager(); + regionManager.Regions.Add(new MockPresentationRegion { Name = "region name" }); + regionManager.Regions.Add(new MockPresentationRegion { Name = "region name" }); + }); + + } + + [Fact] + public void AddPassesItselfAsTheRegionManagerOfTheRegion() + { + var regionManager = new RegionManager(); + var region = new MockPresentationRegion + { + Name = "region" + }; + + regionManager.Regions.Add(region); + + Assert.Same(regionManager, region.RegionManager); + } + + [Fact] + public void CreateRegionManagerCreatesANewInstance() + { + var regionManager = new RegionManager(); + var createdRegionManager = regionManager.CreateRegionManager(); + Assert.NotNull(createdRegionManager); + Assert.IsType(createdRegionManager); + Assert.NotSame(regionManager, createdRegionManager); + } + + [Fact] + public void CanRemoveRegion() + { + var regionManager = new RegionManager(); + IRegion region = new MockPresentationRegion + { + Name = "TestRegion" + }; + + regionManager.Regions.Add(region); + regionManager.Regions.Remove("TestRegion"); + + Assert.False(regionManager.Regions.ContainsRegionWithName("TestRegion")); + } + + [Fact] + public void ShouldRemoveRegionManagerWhenRemoving() + { + var regionManager = new RegionManager(); + var region = new MockPresentationRegion + { + Name = "TestRegion" + }; + + regionManager.Regions.Add(region); + regionManager.Regions.Remove("TestRegion"); + + Assert.Null(region.RegionManager); + } + + [Fact] + public void UpdatingRegionsGetsCalledWhenAccessingRegionMembers() + { + var listener = new MySubscriberClass(); + + try + { + RegionManager.UpdatingRegions += listener.OnUpdatingRegions; + RegionManager regionManager = new RegionManager(); + regionManager.Regions.ContainsRegionWithName("TestRegion"); + Assert.True(listener.OnUpdatingRegionsCalled); + + listener.OnUpdatingRegionsCalled = false; + regionManager.Regions.Add(new MockPresentationRegion() { Name = "TestRegion" }); + Assert.True(listener.OnUpdatingRegionsCalled); + + listener.OnUpdatingRegionsCalled = false; + var region = regionManager.Regions["TestRegion"]; + Assert.True(listener.OnUpdatingRegionsCalled); + + listener.OnUpdatingRegionsCalled = false; + regionManager.Regions.Remove("TestRegion"); + Assert.True(listener.OnUpdatingRegionsCalled); + + listener.OnUpdatingRegionsCalled = false; + regionManager.Regions.GetEnumerator(); + Assert.True(listener.OnUpdatingRegionsCalled); + } + finally + { + RegionManager.UpdatingRegions -= listener.OnUpdatingRegions; + } + } + + [StaFact(Skip = "Avalonia doesn't auto-create ObservableObject in RegionContext")] + public void ShouldSetObservableRegionContextWhenRegionContextChanges() + { + var region = new MockPresentationRegion(); + var view = new MockDependencyObject(); + + var observableObject = RegionContext.GetObservableContext(view); + + bool propertyChangedCalled = false; + observableObject.PropertyChanged += (sender, args) => propertyChangedCalled = true; + + Assert.Null(observableObject.Value); + RegionManager.SetRegionContext(view, "MyContext"); + Assert.True(propertyChangedCalled); + Assert.Equal("MyContext", observableObject.Value); + } + + [Fact] + public async Task ShouldNotPreventSubscribersToStaticEventFromBeingGarbageCollected() + { + var subscriber = new MySubscriberClass(); + RegionManager.UpdatingRegions += subscriber.OnUpdatingRegions; + RegionManager.UpdateRegions(); + Assert.True(subscriber.OnUpdatingRegionsCalled); + WeakReference subscriberWeakReference = new WeakReference(subscriber); + + subscriber = null; + await Task.Delay(50); + GC.Collect(); + + Assert.False(subscriberWeakReference.IsAlive); + } + + [Fact] + public void ExceptionMessageWhenCallingUpdateRegionsShouldBeClear() + { + try + { + ExceptionExtensions.RegisterFrameworkExceptionType(typeof(FrameworkException)); + RegionManager.UpdatingRegions += new EventHandler(RegionManager_UpdatingRegions); + + try + { + RegionManager.UpdateRegions(); + //Assert.Fail(); + } + catch (Exception ex) + { + Assert.Contains("Abcde", ex.Message); + } + } + finally + { + RegionManager.UpdatingRegions -= new EventHandler(RegionManager_UpdatingRegions); + } + } + + private void RegionManager_UpdatingRegions(object sender, EventArgs e) + { + try + { + throw new Exception("Abcde"); + } + catch (Exception ex) + { + throw new FrameworkException(ex); + } + } + + internal class MySubscriberClass + { + public bool OnUpdatingRegionsCalled; + + public void OnUpdatingRegions(object sender, EventArgs e) + { + OnUpdatingRegionsCalled = true; + } + } + + [Fact] + public void WhenAddingRegions_ThenRegionsCollectionNotifiesUpdate() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new RegionManager(); + + var region1 = new Region { Name = "region1" }; + var region2 = new Region { Name = "region2" }; + + NotifyCollectionChangedEventArgs args = null; + regionManager.Regions.CollectionChanged += (s, e) => args = e; + + regionManager.Regions.Add(region1); + + Assert.Equal(NotifyCollectionChangedAction.Add, args.Action); + Assert.Equal(new object[] { region1 }, args.NewItems); + Assert.Equal(0, args.NewStartingIndex); + Assert.Null(args.OldItems); + Assert.Equal(-1, args.OldStartingIndex); + + regionManager.Regions.Add(region2); + + Assert.Equal(NotifyCollectionChangedAction.Add, args.Action); + Assert.Equal(new object[] { region2 }, args.NewItems); + Assert.Equal(0, args.NewStartingIndex); + Assert.Null(args.OldItems); + Assert.Equal(-1, args.OldStartingIndex); + } + + [Fact] + public void WhenRemovingRegions_ThenRegionsCollectionNotifiesUpdate() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new RegionManager(); + + var region1 = new Region { Name = "region1" }; + var region2 = new Region { Name = "region2" }; + + regionManager.Regions.Add(region1); + regionManager.Regions.Add(region2); + + NotifyCollectionChangedEventArgs args = null; + regionManager.Regions.CollectionChanged += (s, e) => args = e; + + regionManager.Regions.Remove("region2"); + + Assert.Equal(NotifyCollectionChangedAction.Remove, args.Action); + Assert.Equal(new object[] { region2 }, args.OldItems); + Assert.Equal(0, args.OldStartingIndex); + Assert.Null(args.NewItems); + Assert.Equal(-1, args.NewStartingIndex); + + regionManager.Regions.Remove("region1"); + + Assert.Equal(NotifyCollectionChangedAction.Remove, args.Action); + Assert.Equal(new object[] { region1 }, args.OldItems); + Assert.Equal(0, args.OldStartingIndex); + Assert.Null(args.NewItems); + Assert.Equal(-1, args.NewStartingIndex); + } + + [Fact] + public void WhenRemovingNonExistingRegion_ThenRegionsCollectionDoesNotNotifyUpdate() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var regionManager = new RegionManager(); + + var region1 = new Region { Name = "region1" }; + + regionManager.Regions.Add(region1); + + NotifyCollectionChangedEventArgs args = null; + regionManager.Regions.CollectionChanged += (s, e) => args = e; + + regionManager.Regions.Remove("region2"); + + Assert.Null(args); + } + + [Fact] + public void CanAddViewToRegion() + { + var regionManager = new RegionManager(); + var view1 = new object(); + var view2 = new object(); + + IRegion region = new MockRegion + { + Name = "RegionName" + }; + + regionManager.Regions.Add(region); + + regionManager.AddToRegion("RegionName", view1); + regionManager.AddToRegion("RegionName", view2); + + Assert.True(regionManager.Regions["RegionName"].Views.Contains(view1)); + Assert.True(regionManager.Regions["RegionName"].Views.Contains(view2)); + } + + [Fact(DisplayName = "Flaky test. Run by itself not as a group.")] + public void CanRegisterViewType() + { + try + { + var mockRegionContentRegistry = new MockRegionContentRegistry(); + + string regionName = null; + Type viewType = null; + + mockRegionContentRegistry.RegisterContentWithViewType = (name, type) => + { + regionName = name; + viewType = type; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var regionManager = new RegionManager(); + + regionManager.RegisterViewWithRegion("Region1", typeof(object)); + + Assert.Equal("Region1", regionName); + Assert.Equal(typeof(object), viewType); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void CanRegisterViewTypeGeneric() + { + try + { + var mockRegionContentRegistry = new MockRegionContentRegistry(); + + string regionName = null; + Type viewType = null; + + mockRegionContentRegistry.RegisterContentWithViewType = (name, type) => + { + regionName = name; + viewType = type; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + ContainerLocator.ResetContainer(); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var regionManager = new RegionManager(); + + regionManager.RegisterViewWithRegion("Region1"); + + Assert.Equal("Region1", regionName); + Assert.Equal(typeof(object), viewType); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void CanRegisterDelegate() + { + try + { + ContainerLocator.ResetContainer(); + var mockRegionContentRegistry = new MockRegionContentRegistry(); + + string regionName = null; + Func contentDelegate = null; + Func expectedDelegate = _ => true; + + mockRegionContentRegistry.RegisterContentWithDelegate = (name, usedDelegate) => + { + regionName = name; + contentDelegate = usedDelegate; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var regionManager = new RegionManager(); + + regionManager.RegisterViewWithRegion("Region1", expectedDelegate); + + Assert.Equal("Region1", regionName); + Assert.Equal(expectedDelegate, contentDelegate); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void CanAddRegionToRegionManager() + { + var regionManager = new RegionManager(); + var region = new MockRegion(); + + regionManager.Regions.Add("region", region); + + Assert.Single(regionManager.Regions); + Assert.Equal("region", region.Name); + } + + [Fact] + public void ShouldThrowIfRegionNameArgumentIsDifferentToRegionNameProperty() + { + var ex = Assert.Throws(() => + { + var regionManager = new RegionManager(); + var region = new MockRegion + { + Name = "region" + }; + + regionManager.Regions.Add("another region", region); + }); + } + } + + internal class FrameworkException : Exception + { + public FrameworkException(Exception inner) + : base(string.Empty, inner) + { + } + } + + internal class MockRegionContentRegistry : IRegionViewRegistry + { + public Func RegisterContentWithViewType; + public Func, object> RegisterContentWithDelegate; + public event EventHandler ContentRegistered; + public IEnumerable GetContents(string regionName, IContainerProvider container) + { + return null; + } + + public void RegisterViewWithRegion(string regionName, string targetName) + { + throw new NotImplementedException(); + } + + void IRegionViewRegistry.RegisterViewWithRegion(string regionName, Type viewType) + { + RegisterContentWithViewType?.Invoke(regionName, viewType); + } + + void IRegionViewRegistry.RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + RegisterContentWithDelegate?.Invoke(regionName, getContentDelegate); + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerRequestNavigateFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerRequestNavigateFixture.cs new file mode 100644 index 0000000000..e6deb70153 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerRequestNavigateFixture.cs @@ -0,0 +1,129 @@ +using System; +using Moq; +using Prism.Navigation; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionManagerRequestNavigateFixture + { + const string region = "Region"; + const string nonExistentRegion = "NonExistentRegion"; + const string source = "Source"; + + private static Uri sourceUri = new Uri(source, UriKind.RelativeOrAbsolute); + private static NavigationParameters parameters = new NavigationParameters(); + private static Action callback = (_) => { }; + + private static Mock mockRegion; + private static RegionManager regionManager; + + public RegionManagerRequestNavigateFixture() + { + mockRegion = new Mock(); + mockRegion.SetupGet((r) => r.Name).Returns(region); + + regionManager = new RegionManager(); + regionManager.Regions.Add(mockRegion.Object); + } + + [Fact] + public void ThrowsWhenNavigationCallbackIsNull() + { + ExceptionAssert.Throws(() => + regionManager.RequestNavigate(region, source, null, parameters) + ); + + ExceptionAssert.Throws(() => + regionManager.RequestNavigate(region, source, navigationCallback: null) + ); + + ExceptionAssert.Throws(() => + regionManager.RequestNavigate(region, sourceUri, null, parameters) + ); + + ExceptionAssert.Throws(() => + regionManager.RequestNavigate(region, sourceUri, navigationCallback: null) + ); + } + + [Fact] + public void WhenNonExistentRegion_ReturnNavigationResultFalse() + { + NavigationResult result; + + result = null; + regionManager.RequestNavigate(nonExistentRegion, source, (r) => result = r, parameters); + Assert.False(result.Success); + + result = null; + regionManager.RequestNavigate(nonExistentRegion, source, (r) => result = r); + Assert.False(result.Success); + + result = null; + regionManager.RequestNavigate(nonExistentRegion, sourceUri, (r) => result = r, parameters); + Assert.False(result.Success); + + result = null; + regionManager.RequestNavigate(nonExistentRegion, sourceUri, (r) => result = r); + Assert.False(result.Success); + } + + [Fact] + public void DelegatesCallToRegion_RegionSource() + { + regionManager.RequestNavigate(region, source); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, It.IsAny>(), It.IsAny())); + } + + [Fact] + public void DelegatesCallToRegion_RegionTarget() + { + regionManager.RequestNavigate(region, sourceUri); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, It.IsAny>(), It.IsAny())); + } + + [Fact] + public void DelegatesCallToRegion_RegionSourceParameters() + { + regionManager.RequestNavigate(region, source, parameters); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, It.IsAny>(), parameters)); + } + + [Fact] + public void DelegatesCallToRegion_RegionSourceUriParameters() + { + regionManager.RequestNavigate(region, sourceUri, parameters); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, It.IsAny>(), parameters)); + } + + [Fact] + public void DelegatesCallToRegion_RegionSourceCallback() + { + regionManager.RequestNavigate(region, source, callback); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, callback, It.IsAny())); + } + + [Fact] + public void DelegatesCallToRegion_RegionTargetCallback() + { + regionManager.RequestNavigate(region, sourceUri, callback); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, callback, It.IsAny())); + } + + [Fact] + public void DelegatesCallToRegion_RegionSourceCallbackParameters() + { + regionManager.RequestNavigate(region, source, callback, parameters); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, callback, parameters)); + } + + [Fact] + public void DelegatesCallToRegion_RegionSourceUriCallbackParameters() + { + regionManager.RequestNavigate(region, sourceUri, callback, parameters); + mockRegion.Verify((r) => r.RequestNavigate(sourceUri, callback, parameters)); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationJournalFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationJournalFixture.cs new file mode 100644 index 0000000000..bd2f2e08f9 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationJournalFixture.cs @@ -0,0 +1,490 @@ +using System; +using Moq; +using Prism.Navigation; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionNavigationJournalFixture + { + [Fact] + public void ConstructingJournalInitializesValues() + { + // Act + RegionNavigationJournal target = new RegionNavigationJournal(); + + // Verify + Assert.False(target.CanGoBack); + Assert.False(target.CanGoForward); + Assert.Null(target.CurrentEntry); + Assert.Null(target.NavigationTarget); + } + + [Fact] + public void SettingNavigationServiceUpdatesValue() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockINavigate = new Mock(); + + // Act + target.NavigationTarget = mockINavigate.Object; + + // Verify + Assert.Same(mockINavigate.Object, target.NavigationTarget); + } + + [Fact] + public void RecordingNavigationUpdatesNavigationState() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Uri uri = new Uri("Uri", UriKind.Relative); + RegionNavigationJournalEntry entry = new RegionNavigationJournalEntry() { Uri = uri }; + + // Act + target.RecordNavigation(entry, true); + + // Verify + Assert.False(target.CanGoBack); + Assert.False(target.CanGoForward); + Assert.Same(entry, target.CurrentEntry); + } + + [Fact] + public void RecordingNavigationMultipleTimesUpdatesNavigationState() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + // Act + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + // Verify + Assert.True(target.CanGoBack); + Assert.False(target.CanGoForward); + Assert.Same(entry3, target.CurrentEntry); + } + + [Fact] + public void ClearUpdatesNavigationState() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + // Act + target.Clear(); + + // Verify + Assert.False(target.CanGoBack); + Assert.False(target.CanGoForward); + Assert.Null(target.CurrentEntry); + } + + [Fact] + public void GoBackNavigatesBack() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + // Act + target.GoBack(); + + // Verify + Assert.True(target.CanGoBack); + Assert.True(target.CanGoForward); + Assert.Same(entry2, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Never()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Never()); + } + + [Fact] + public void GoBackDoesNotChangeStateWhenNavigationFails() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, false))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + // Act + target.GoBack(); + + // Verify + Assert.True(target.CanGoBack); + Assert.False(target.CanGoForward); + Assert.Same(entry3, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Never()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Never()); + } + + [Fact] + public void GoBackMultipleTimesNavigatesBack() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + // Act + target.GoBack(); + target.GoBack(); + + // Verify + Assert.False(target.CanGoBack); + Assert.True(target.CanGoForward); + Assert.Same(entry1, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Never()); + } + + [Fact] + public void GoForwardNavigatesForward() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + target.GoBack(); + target.GoBack(); + + // Act + target.GoForward(); + + // Verify + Assert.True(target.CanGoBack); + Assert.True(target.CanGoForward); + Assert.Same(entry2, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Exactly(2)); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Never()); + } + + [Fact] + public void GoForwardDoesNotChangeStateWhenNavigationFails() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, false))); + + target.GoBack(); + + // Act + target.GoForward(); + + // Verify + Assert.True(target.CanGoBack); + Assert.True(target.CanGoForward); + Assert.Same(entry2, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Never()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Once()); + } + + [Fact] + public void GoForwardMultipleTimesNavigatesForward() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + target.GoBack(); + target.GoBack(); + + // Act + target.GoForward(); + target.GoForward(); + + // Verify + Assert.True(target.CanGoBack); + Assert.False(target.CanGoForward); + Assert.Same(entry3, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Exactly(2)); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Once()); + } + + [Fact] + public void WhenNavigationToNewUri_ThenCanNoLongerNavigateForward() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + Uri uri4 = new Uri("Uri4", UriKind.Relative); + RegionNavigationJournalEntry entry4 = new RegionNavigationJournalEntry() { Uri = uri4 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + target.GoBack(); + + Assert.True(target.CanGoForward); + + // Act + target.RecordNavigation(entry3, true); + + // Verify + Assert.False(target.CanGoForward); + Assert.Equal(entry3, target.CurrentEntry); + } + + [Fact] + public void WhenSavePreviousFalseDoNotRecordEntry() + { + // Prepare + RegionNavigationJournal target = new RegionNavigationJournal(); + + Mock mockNavigationTarget = new Mock(); + target.NavigationTarget = mockNavigationTarget.Object; + + Uri uri1 = new Uri("Uri1", UriKind.Relative); + RegionNavigationJournalEntry entry1 = new RegionNavigationJournalEntry() { Uri = uri1 }; + + Uri uri2 = new Uri("Uri2", UriKind.Relative); + RegionNavigationJournalEntry entry2 = new RegionNavigationJournalEntry() { Uri = uri2 }; + + Uri uri3 = new Uri("Uri3", UriKind.Relative); + RegionNavigationJournalEntry entry3 = new RegionNavigationJournalEntry() { Uri = uri3 }; + + Uri uri4 = new Uri("Uri4", UriKind.Relative); + RegionNavigationJournalEntry entry4 = new RegionNavigationJournalEntry() { Uri = uri4 }; + + target.RecordNavigation(entry1, true); + target.RecordNavigation(entry2, true); + target.RecordNavigation(entry3, false); + target.RecordNavigation(entry4, true); + + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri1, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri2, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri3, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + mockNavigationTarget + .Setup(x => x.RequestNavigate(uri4, It.IsAny>(), null)) + .Callback, INavigationParameters>((u, c, n) => c(new NavigationResult(null, true))); + + Assert.Equal(entry4, target.CurrentEntry); + + target.GoBack(); + + Assert.True(target.CanGoBack); + Assert.True(target.CanGoForward); + Assert.Same(entry2, target.CurrentEntry); + + mockNavigationTarget.Verify(x => x.RequestNavigate(uri1, It.IsAny>(), null), Times.Never()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri2, It.IsAny>(), null), Times.Once()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri3, It.IsAny>(), null), Times.Never()); + mockNavigationTarget.Verify(x => x.RequestNavigate(uri4, It.IsAny>(), null), Times.Never()); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationServiceFixture.new.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationServiceFixture.new.cs new file mode 100644 index 0000000000..3598fef35e --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationServiceFixture.new.cs @@ -0,0 +1,1194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Moq; +using Prism.Ioc; +using Prism.Navigation; +using Prism.Navigation.Regions; +using Xunit; +namespace Prism.Avalonia.Tests.Regions +{ + public class RegionNavigationServiceFixture + { + [Fact] + public void WhenNavigating_ViewIsActivated() + { + // Prepare + object view = new object(); + Uri viewUri = new Uri(view.GetType().Name, UriKind.Relative); + + IRegion region = new Region(); + region.Add(view); + + string regionName = "RegionName"; + RegionManager regionManager = new RegionManager(); + regionManager.Regions.Add(regionName, region); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + bool isNavigationSuccessful = false; + target.RequestNavigate(viewUri, nr => isNavigationSuccessful = nr.Success == true); + + // Verify + Assert.True(isNavigationSuccessful); + bool isViewActive = region.ActiveViews.Contains(view); + Assert.True(isViewActive); + } + + [Fact] + public void WhenNavigatingWithQueryString_ViewIsActivated() + { + // Prepare + object view = new object(); + Uri viewUri = new Uri(view.GetType().Name + "?MyQuery=true", UriKind.Relative); + + IRegion region = new Region(); + region.Add(view); + + string regionName = "RegionName"; + RegionManager regionManager = new RegionManager(); + regionManager.Regions.Add(regionName, region); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + bool isNavigationSuccessful = false; + target.RequestNavigate(viewUri, nr => isNavigationSuccessful = nr.Success == true); + + // Verify + Assert.True(isNavigationSuccessful); + bool isViewActive = region.ActiveViews.Contains(view); + Assert.True(isViewActive); + } + + [Fact] + public void WhenNavigatingAndViewCannotBeAcquired_ThenNavigationResultHasError() + { + // Prepare + object view = new object(); + Uri viewUri = new Uri(view.GetType().Name, UriKind.Relative); + + IRegion region = new Region(); + region.Add(view); + + string otherType = "OtherType"; + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + IContainerExtension container = containerMock.Object; + + Mock targetHandlerMock = new Mock(); + targetHandlerMock.Setup(th => th.LoadContent(It.IsAny(), It.IsAny())).Throws(); + + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, targetHandlerMock.Object, journal) + { + Region = region + }; + + // Act + Exception error = null; + target.RequestNavigate( + new Uri(otherType.GetType().Name, UriKind.Relative), + nr => + { + error = nr.Exception; + }); + + // Verify + bool isViewActive = region.ActiveViews.Contains(view); + Assert.False(isViewActive); + Assert.IsType(error); + } + + [Fact] + public void WhenNavigatingWithNullUri_Throws() + { + // Prepare + IRegion region = new Region(); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + NavigationResult navigationResult = null; + target.RequestNavigate((Uri)null, nr => navigationResult = nr); + + // Verify + Assert.False(navigationResult.Success); + Assert.NotNull(navigationResult.Exception); + Assert.IsType(navigationResult.Exception); + } + + [Fact] + public void WhenNavigatingAndViewImplementsINavigationAware_ThenNavigatedIsInvokedOnNavigation() + { + // Prepare + var region = new Region(); + + var viewMock = new Mock(); + viewMock.Setup(ina => ina.IsNavigationTarget(It.IsAny())).Returns(true); + var view = viewMock.Object; + region.Add(view); + + var navigationUri = new Uri(view.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + viewMock.Verify(v => v.OnNavigatedTo(It.Is(nc => nc.Uri == navigationUri && nc.NavigationService == target))); + } + + [StaFact] + public void WhenNavigatingAndDataContextImplementsINavigationAware_ThenNavigatedIsInvokesOnNavigation() + { + // Prepare + var region = new Region(); + + Mock mockControl = new Mock(); + Mock mockINavigationAwareDataContext = new Mock(); + mockINavigationAwareDataContext.Setup(ina => ina.IsNavigationTarget(It.IsAny())).Returns(true); + mockControl.Object.DataContext = mockINavigationAwareDataContext.Object; + + var view = mockControl.Object; + region.Add(view); + + var navigationUri = new Uri(view.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + mockINavigationAwareDataContext.Verify(v => v.OnNavigatedTo(It.Is(nc => nc.Uri == navigationUri))); + } + + [StaFact(Skip = "Type to mock (Avalonia.Controls.Control) must be an interface, a delegate, or a non-sealed, non-static class.")] + public void WhenNavigatingAndBothViewAndDataContextImplementINavigationAware_ThenNavigatedIsInvokesOnNavigation() + { + // Prepare + var region = new Region(); + + Mock mockControl = new Mock(); + Mock mockINavigationAwareView = mockControl.As(); + mockINavigationAwareView.Setup(ina => ina.IsNavigationTarget(It.IsAny())).Returns(true); + + Mock mockINavigationAwareDataContext = new Mock(); + mockINavigationAwareDataContext.Setup(ina => ina.IsNavigationTarget(It.IsAny())).Returns(true); + mockControl.Object.DataContext = mockINavigationAwareDataContext.Object; + + var view = mockControl.Object; + region.Add(view); + + var navigationUri = new Uri(view.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + mockINavigationAwareView.Verify(v => v.OnNavigatedTo(It.Is(nc => nc.Uri == navigationUri))); + mockINavigationAwareDataContext.Verify(v => v.OnNavigatedTo(It.Is(nc => nc.Uri == navigationUri))); + } + + [Fact] + public void WhenNavigating_NavigationIsRecordedInJournal() + { + // Prepare + object view = new object(); + Uri viewUri = new Uri(view.GetType().Name, UriKind.Relative); + + IRegion region = new Region(); + region.Add(view); + + string regionName = "RegionName"; + RegionManager regionManager = new RegionManager(); + regionManager.Regions.Add(regionName, region); + + IRegionNavigationJournalEntry journalEntry = new RegionNavigationJournalEntry(); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(journalEntry); + + IContainerExtension container = containerMock.Object; + ContainerLocator.SetContainerExtension(container); + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + + var journalMock = new Mock(); + journalMock.Setup(x => x.RecordNavigation(journalEntry, true)).Verifiable(); + + IRegionNavigationJournal journal = journalMock.Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(viewUri, nr => { }); + + // Verify + Assert.NotNull(journalEntry); + Assert.Equal(viewUri, journalEntry.Uri); + journalMock.VerifyAll(); + } + + [Fact] + public void WhenNavigatingAndCurrentlyActiveViewImplementsINavigateWithVeto_ThenNavigationRequestQueriesForVeto() + { + // Prepare + var region = new Region(); + + var viewMock = new Mock(); + viewMock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Verifiable(); + + var view = viewMock.Object; + region.Add(view); + region.Activate(view); + + var navigationUri = new Uri(view.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + viewMock.VerifyAll(); + } + + [Fact] + public void WhenNavigating_ThenNavigationRequestQueriesForVetoOnAllActiveViewsIfAllSucceed() + { + // Prepare + var region = new Region(); + + var view1Mock = new Mock(); + view1Mock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(true)) + .Verifiable(); + + var view1 = view1Mock.Object; + region.Add(view1); + region.Activate(view1); + + var view2Mock = new Mock(); + + var view2 = view2Mock.Object; + region.Add(view2); + + var view3Mock = new Mock(); + + var view3 = view3Mock.Object; + region.Add(view3); + region.Activate(view3); + + var view4Mock = new Mock(); + view4Mock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(true)) + .Verifiable(); + + var view4 = view4Mock.Object; + region.Add(view4); + region.Activate(view4); + + var navigationUri = new Uri(view1.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + view1Mock.VerifyAll(); + view2Mock.Verify(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>()), Times.Never()); + view3Mock.VerifyAll(); + view4Mock.VerifyAll(); + } + + [Fact] + public void WhenRequestNavigateAwayAcceptsThroughCallback_ThenNavigationProceeds() + { + // Prepare + var region = new Region(); + + var view1Mock = new Mock(); + view1Mock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(true)) + .Verifiable(); + + var view1 = view1Mock.Object; + + var view2 = new object(); + + region.Add(view1); + region.Add(view2); + + region.Activate(view1); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + var navigationSucceeded = false; + target.RequestNavigate(navigationUri, nr => { navigationSucceeded = nr.Success == true; }); + + // Verify + view1Mock.VerifyAll(); + Assert.True(navigationSucceeded); + Assert.Equal(new object[] { view1, view2 }, region.ActiveViews.ToArray()); + } + + [Fact] + public void WhenRequestNavigateAwayRejectsThroughCallback_ThenNavigationDoesNotProceed() + { + // Prepare + var region = new Region(); + + var view1Mock = new Mock(); + view1Mock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(false)) + .Verifiable(); + + var view1 = view1Mock.Object; + + var view2 = new object(); + + region.Add(view1); + region.Add(view2); + + region.Activate(view1); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + var navigationFailed = false; + target.RequestNavigate(navigationUri, nr => { navigationFailed = nr.Success == false; }); + + // Verify + view1Mock.VerifyAll(); + Assert.True(navigationFailed); + Assert.Equal(new object[] { view1 }, region.ActiveViews.ToArray()); + } + + [StaFact] + public void WhenNavigatingAndDataContextOnCurrentlyActiveViewImplementsINavigateWithVeto_ThenNavigationRequestQueriesForVeto() + { + // Prepare + var region = new Region(); + + var viewModelMock = new Mock(); + viewModelMock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Verifiable(); + + var viewMock = new Mock(); + + var view = viewMock.Object; + view.DataContext = viewModelMock.Object; + + region.Add(view); + region.Activate(view); + + var navigationUri = new Uri(view.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + viewModelMock.VerifyAll(); + } + + [StaFact] + public void WhenRequestNavigateAwayOnDataContextAcceptsThroughCallback_ThenNavigationProceeds() + { + // Prepare + var region = new Region(); + + var view1DataContextMock = new Mock(); + view1DataContextMock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(true)) + .Verifiable(); + + var view1Mock = new Mock(); + var view1 = view1Mock.Object; + view1.DataContext = view1DataContextMock.Object; + + var view2 = new object(); + + region.Add(view1); + region.Add(view2); + + region.Activate(view1); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + var navigationSucceeded = false; + target.RequestNavigate(navigationUri, nr => { navigationSucceeded = nr.Success == true; }); + + // Verify + view1DataContextMock.VerifyAll(); + Assert.True(navigationSucceeded); + Assert.Equal(new object[] { view1, view2 }, region.ActiveViews.ToArray()); + } + + [StaFact] + public void WhenRequestNavigateAwayOnDataContextRejectsThroughCallback_ThenNavigationDoesNotProceed() + { + // Prepare + var region = new Region(); + + var view1DataContextMock = new Mock(); + view1DataContextMock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(false)) + .Verifiable(); + + var view1Mock = new Mock(); + var view1 = view1Mock.Object; + view1.DataContext = view1DataContextMock.Object; + + var view2 = new object(); + + region.Add(view1); + region.Add(view2); + + region.Activate(view1); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + var navigationFailed = false; + target.RequestNavigate(navigationUri, nr => { navigationFailed = nr.Success == false; }); + + // Verify + view1DataContextMock.VerifyAll(); + Assert.True(navigationFailed); + Assert.Equal(new object[] { view1 }, region.ActiveViews.ToArray()); + } + + [Fact] + public void WhenViewAcceptsNavigationOutAfterNewIncomingRequestIsReceived_ThenOriginalRequestIsIgnored() + { + var region = new Region(); + + var viewMock = new Mock(); + var view = viewMock.Object; + + var confirmationRequests = new List>(); + + viewMock + .Setup(icnr => icnr.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => { confirmationRequests.Add(c); }); + + region.Add(view); + region.Activate(view); + + var navigationUri = new Uri("", UriKind.Relative); + + var containerMock = new Mock(); + containerMock + .Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))) + .Returns(new RegionNavigationJournalEntry()); + + var contentLoaderMock = new Mock(); + contentLoaderMock + .Setup(cl => cl.LoadContent(region, It.IsAny())) + .Returns(view); + + var container = containerMock.Object; + var contentLoader = contentLoaderMock.Object; + var journal = new Mock().Object; + + var target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + bool firstNavigation = false; + bool secondNavigation = false; + target.RequestNavigate(navigationUri, nr => firstNavigation = nr.Success); + target.RequestNavigate(navigationUri, nr => secondNavigation = nr.Success); + + Assert.Equal(2, confirmationRequests.Count); + + confirmationRequests[0](true); + confirmationRequests[1](true); + + Assert.False(firstNavigation); + Assert.True(secondNavigation); + } + + [StaFact] + public void WhenViewModelAcceptsNavigationOutAfterNewIncomingRequestIsReceived_ThenOriginalRequestIsIgnored() + { + var region = new Region(); + + var viewModelMock = new Mock(); + + var viewMock = new Mock(); + var view = viewMock.Object; + view.DataContext = viewModelMock.Object; + + var confirmationRequests = new List>(); + + viewModelMock + .Setup(icnr => icnr.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => { confirmationRequests.Add(c); }); + + region.Add(view); + region.Activate(view); + + var navigationUri = new Uri("", UriKind.Relative); + + var containerMock = new Mock(); + containerMock + .Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))) + .Returns(new RegionNavigationJournalEntry()); + + var contentLoaderMock = new Mock(); + contentLoaderMock + .Setup(cl => cl.LoadContent(region, It.IsAny())) + .Returns(view); + + var container = containerMock.Object; + var contentLoader = contentLoaderMock.Object; + var journal = new Mock().Object; + + var target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + bool firstNavigation = false; + bool secondNavigation = false; + target.RequestNavigate(navigationUri, nr => firstNavigation = nr.Success); + target.RequestNavigate(navigationUri, nr => secondNavigation = nr.Success); + + Assert.Equal(2, confirmationRequests.Count); + + confirmationRequests[0](true); + confirmationRequests[1](true); + + Assert.False(firstNavigation); + Assert.True(secondNavigation); + } + + [Fact] + public void BeforeNavigating_NavigatingEventIsRaised() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + // Prepare + object view = new object(); + Uri viewUri = new Uri(view.GetType().Name, UriKind.Relative); + + IRegion region = new Region(); + region.Add(view); + + string regionName = "RegionName"; + RegionManager regionManager = new RegionManager(); + regionManager.Regions.Add(regionName, region); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + bool isNavigatingRaised = false; + target.Navigating += delegate (object sender, RegionNavigationEventArgs e) + { + if (sender == target) + { + isNavigatingRaised = true; + } + }; + + // Act + bool isNavigationSuccessful = false; + target.RequestNavigate(viewUri, nr => isNavigationSuccessful = nr.Success == true); + + // Verify + Assert.True(isNavigationSuccessful); + Assert.True(isNavigatingRaised); + } + + [Fact] + public void WhenNavigationSucceeds_NavigatedIsRaised() + { + // Prepare + object view = new object(); + Uri viewUri = new Uri(view.GetType().Name, UriKind.Relative); + + IRegion region = new Region(); + region.Add(view); + + string regionName = "RegionName"; + RegionManager regionManager = new RegionManager(); + regionManager.Regions.Add(regionName, region); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new RegionNavigationContentLoader(container); + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + bool isNavigatedRaised = false; + target.Navigated += delegate (object sender, RegionNavigationEventArgs e) + { + if (sender == target) + { + isNavigatedRaised = true; + } + }; + + // Act + bool isNavigationSuccessful = false; + target.RequestNavigate(viewUri, nr => isNavigationSuccessful = nr.Success == true); + + // Verify + Assert.True(isNavigationSuccessful); + Assert.True(isNavigatedRaised); + } + + [Fact] + public void WhenTargetViewCreationThrowsWithAsyncConfirmation_ThenExceptionIsProvidedToNavigationCallback() + { + var containerMock = new Mock(); + + var targetException = new Exception(); + var targetHandlerMock = new Mock(); + targetHandlerMock + .Setup(th => th.LoadContent(It.IsAny(), It.IsAny())) + .Throws(targetException); + + var journalMock = new Mock(); + + Action navigationCallback = null; + var viewMock = new Mock(); + viewMock + .Setup(v => v.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => { navigationCallback = c; }); + + var region = new Region(); + region.Add(viewMock.Object); + region.Activate(viewMock.Object); + + var target = new RegionNavigationService(containerMock.Object, targetHandlerMock.Object, journalMock.Object) + { + Region = region + }; + + NavigationResult result = null; + target.RequestNavigate(new Uri("", UriKind.Relative), nr => result = nr); + navigationCallback(true); + + Assert.NotNull(result); + Assert.Same(targetException, result.Exception); + } + + [Fact] + public void WhenNavigatingFromViewThatIsNavigationAware_ThenNotifiesActiveViewNavigatingFrom() + { + // Arrange + var region = new Region(); + var viewMock = new Mock(); + var view = viewMock.Object; + region.Add(view); + + var view2 = new object(); + region.Add(view2); + + region.Activate(view); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + viewMock.Verify(v => v.OnNavigatedFrom(It.Is(ctx => ctx.Uri == navigationUri && ctx.Parameters.Count() == 0))); + } + + [Fact] + public void WhenNavigationFromViewThatIsNavigationAware_OnlyNotifiesOnNavigateFromForActiveViews() + { + // Arrange + + bool navigationFromInvoked = false; + + var region = new Region(); + + var viewMock = new Mock(); + viewMock + .Setup(x => x.OnNavigatedFrom(It.IsAny())).Callback(() => navigationFromInvoked = true); + var view = viewMock.Object; + region.Add(view); + + var targetViewMock = new Mock(); + region.Add(targetViewMock.Object); + + var activeViewMock = new Mock(); + region.Add(activeViewMock.Object); + + region.Activate(activeViewMock.Object); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var navigationUri = new Uri(targetViewMock.Object.GetType().Name, UriKind.Relative); + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + Assert.False(navigationFromInvoked); + } + + [StaFact] + public void WhenNavigatingFromActiveViewWithNavigatinAwareDataConext_NotifiesContextOfNavigatingFrom() + { + // Arrange + var region = new Region(); + + var mockDataContext = new Mock(); + + var view1Mock = new Mock(); + var view1 = view1Mock.Object; + view1.DataContext = mockDataContext.Object; + + region.Add(view1); + + var view2 = new object(); + region.Add(view2); + + region.Activate(view1); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + IContainerExtension container = containerMock.Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + // Act + target.RequestNavigate(navigationUri, nr => { }); + + // Verify + mockDataContext.Verify(v => v.OnNavigatedFrom(It.Is(ctx => ctx.Uri == navigationUri && ctx.Parameters.Count() == 0))); + } + + [Fact] + public void WhenNavigatingWithNullCallback_ThenThrows() + { + var region = new Region(); + + var navigationUri = new Uri("/", UriKind.Relative); + IContainerExtension container = new Mock().Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + ExceptionAssert.Throws( + () => + { + target.RequestNavigate(navigationUri, null); + }); + } + + [Fact] + public void WhenNavigatingWithNoRegionSet_ThenMarshallExceptionToCallback() + { + var navigationUri = new Uri("/", UriKind.Relative); + IContainerExtension container = new Mock().Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal); + + Exception error = null; + target.RequestNavigate(navigationUri, nr => error = nr.Exception); + + Assert.NotNull(error); + Assert.IsType(error); + } + + [Fact] + public void WhenNavigatingWithNullUri_ThenMarshallExceptionToCallback() + { + IContainerExtension container = new Mock().Object; + RegionNavigationContentLoader contentLoader = new Mock(container).Object; + IRegionNavigationJournal journal = new Mock().Object; + + RegionNavigationService target = new RegionNavigationService(container, contentLoader, journal) + { + Region = new Region() + }; + + Exception error = null; + target.RequestNavigate(null, nr => error = nr.Exception); + + Assert.NotNull(error); + Assert.IsType(error); + } + + [Fact] + public void WhenNavigationFailsBecauseTheContentViewCannotBeRetrieved_ThenNavigationFailedIsRaised() + { + // Prepare + var region = new Region { Name = "RegionName" }; + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var contentLoaderMock = new Mock(); + contentLoaderMock + .Setup(cl => cl.LoadContent(region, It.IsAny())) + .Throws(); + + var container = containerMock.Object; + var contentLoader = contentLoaderMock.Object; + var journal = new Mock().Object; + + var target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + RegionNavigationFailedEventArgs eventArgs = null; + target.NavigationFailed += delegate (object sender, RegionNavigationFailedEventArgs e) + { + if (sender == target) + { + eventArgs = e; + } + }; + + // Act + bool? isNavigationSuccessful = null; + target.RequestNavigate(new Uri("invalid", UriKind.Relative), nr => isNavigationSuccessful = nr.Success); + + // Verify + Assert.False(isNavigationSuccessful.Value); + Assert.NotNull(eventArgs); + Assert.NotNull(eventArgs.Error); + } + + [Fact] + public void WhenNavigationFailsBecauseActiveViewRejectsIt_ThenNavigationFailedIsRaised() + { + // Prepare + var region = new Region { Name = "RegionName" }; + + var view1Mock = new Mock(); + view1Mock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(false)) + .Verifiable(); + + var view1 = view1Mock.Object; + + var view2 = new object(); + + region.Add(view1); + region.Add(view2); + + region.Activate(view1); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var contentLoaderMock = new Mock(); + contentLoaderMock + .Setup(cl => cl.LoadContent(region, It.IsAny())) + .Returns(view2); + + var container = containerMock.Object; + var contentLoader = contentLoaderMock.Object; + var journal = new Mock().Object; + + var target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + RegionNavigationFailedEventArgs eventArgs = null; + target.NavigationFailed += delegate (object sender, RegionNavigationFailedEventArgs e) + { + if (sender == target) + { + eventArgs = e; + } + }; + + // Act + bool? isNavigationSuccessful = null; + target.RequestNavigate(navigationUri, nr => isNavigationSuccessful = nr.Success); + + // Verify + view1Mock.VerifyAll(); + Assert.False(isNavigationSuccessful.Value); + Assert.NotNull(eventArgs); + Assert.Null(eventArgs.Error); + } + + [StaFact] + public void WhenNavigationFailsBecauseDataContextForActiveViewRejectsIt_ThenNavigationFailedIsRaised() + { + // Prepare + var region = new Region { Name = "RegionName" }; + + var viewModel1Mock = new Mock(); + viewModel1Mock + .Setup(ina => ina.ConfirmNavigationRequest(It.IsAny(), It.IsAny>())) + .Callback>((nc, c) => c(false)) + .Verifiable(); + + var view1Mock = new Mock(); + var view1 = view1Mock.Object; + view1.DataContext = viewModel1Mock.Object; + + var view2 = new object(); + + region.Add(view1); + region.Add(view2); + + region.Activate(view1); + + var navigationUri = new Uri(view2.GetType().Name, UriKind.Relative); + + var containerMock = new Mock(); + containerMock.Setup(x => x.Resolve(typeof(IRegionNavigationJournalEntry))).Returns(new RegionNavigationJournalEntry()); + + var contentLoaderMock = new Mock(); + contentLoaderMock + .Setup(cl => cl.LoadContent(region, It.IsAny())) + .Returns(view2); + + var container = containerMock.Object; + var contentLoader = contentLoaderMock.Object; + var journal = new Mock().Object; + + var target = new RegionNavigationService(container, contentLoader, journal) + { + Region = region + }; + + RegionNavigationFailedEventArgs eventArgs = null; + target.NavigationFailed += delegate (object sender, RegionNavigationFailedEventArgs e) + { + if (sender == target) + { + eventArgs = e; + } + }; + + // Act + bool? isNavigationSuccessful = null; + target.RequestNavigate(navigationUri, nr => isNavigationSuccessful = nr.Success); + + // Verify + viewModel1Mock.VerifyAll(); + Assert.False(isNavigationSuccessful.Value); + Assert.NotNull(eventArgs); + Assert.Null(eventArgs.Error); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs new file mode 100644 index 0000000000..7db7ce8ce7 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs @@ -0,0 +1,201 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Moq; +using Prism.Avalonia.Tests.Mvvm; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions; + +public class RegionViewRegistryFixture +{ + [Fact] + public void CanRegisterContentAndRetrieveIt() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + var result = registry.GetContents("MyRegion"); + + Assert.NotNull(result); + Assert.Single(result); + Assert.IsType(result.ElementAt(0)); + } + + [Fact] + public void ShouldRaiseEventWhenAddingContent() + { + var listener = new MySubscriberClass(); + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.ContentRegistered += listener.OnContentRegistered; + + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + Assert.NotNull(listener.onViewRegisteredArguments); + Assert.NotNull(listener.onViewRegisteredArguments.GetView); + + var result = listener.onViewRegisteredArguments.GetView(containerMock.Object); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void CanRegisterContentAsDelegateAndRetrieveIt() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var registry = new RegionViewRegistry(null); + var content = new MockContentObject(); + + registry.RegisterViewWithRegion("MyRegion", () => content); + var result = registry.GetContents("MyRegion"); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Same(content, result.ElementAt(0)); + } + + [Fact] + public async Task ShouldNotPreventSubscribersFromBeingGarbageCollected() + { + var registry = new RegionViewRegistry(null); + var subscriber = new MySubscriberClass(); + registry.ContentRegistered += subscriber.OnContentRegistered; + + WeakReference subscriberWeakReference = new WeakReference(subscriber); + + subscriber = null; + await Task.Delay(50); + GC.Collect(); + + Assert.False(subscriberWeakReference.IsAlive); + } + + [Fact] + public void OnRegisterErrorShouldGiveClearException() + { + var registry = new RegionViewRegistry(null); + registry.ContentRegistered += new EventHandler(FailWithInvalidOperationException); + + try + { + registry.RegisterViewWithRegion("R1", typeof(object)); + //Assert.Fail(); + } + catch (ViewRegistrationException ex) + { + Assert.Contains("Dont do this", ex.Message); + Assert.Contains("R1", ex.Message); + Assert.Equal("Dont do this", ex.InnerException.Message); + } + catch (Exception) + { + //Assert.Fail("Wrong exception type"); + } + } + + [Fact] + public void OnRegisterErrorShouldSkipFrameworkExceptions() + { + ExceptionExtensions.RegisterFrameworkExceptionType(typeof(FrameworkException)); + var registry = new RegionViewRegistry(null); + registry.ContentRegistered += new EventHandler(FailWithFrameworkException); + var ex = Record.Exception(() => registry.RegisterViewWithRegion("R1", typeof(object))); + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.Contains("Dont do this", ex.Message); + Assert.Contains("R1", ex.Message); + } + + [StaFact(Skip = "Runs alone but not in a group")] + public void RegisterViewWithRegion_ShouldHaveViewModel_ByDefault() + { + ViewModelLocatorFixture.ResetViewModelLocationProvider(); + + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(Mocks.Views.Mock))).Returns(new Mocks.Views.Mock()); + containerMock.Setup(c => c.Resolve(typeof(Mocks.ViewModels.MockViewModel))).Returns(new Mocks.ViewModels.MockViewModel()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", typeof(Mocks.Views.Mock)); + + var result = registry.GetContents("MyRegion"); + Assert.NotNull(result); + Assert.Single(result); + + var view = result.ElementAt(0) as Control; + Assert.IsType(view); + Assert.NotNull(view.DataContext); + Assert.IsType(view.DataContext); + } + + [StaFact(DisplayName = "Flaky test, runs alone but not in a group")] + public void RegisterViewWithRegion_ShouldNotHaveViewModel_OnOptOut() + { + ViewModelLocatorFixture.ResetViewModelLocationProvider(); + + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(Mocks.Views.MockOptOut))).Returns(new Mocks.Views.MockOptOut()); + containerMock.Setup(c => c.Resolve(typeof(Mocks.ViewModels.MockOptOutViewModel))).Returns(new Mocks.ViewModels.MockOptOutViewModel()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", typeof(Mocks.Views.MockOptOut)); + + var result = registry.GetContents("MyRegion"); + Assert.NotNull(result); + Assert.Single(result); + + var view = result.ElementAt(0) as Control; + Assert.IsType(view); + Assert.NotNull(view.DataContext); + } + + private void FailWithFrameworkException(object sender, ViewRegisteredEventArgs e) + { + try + { + FailWithInvalidOperationException(sender, e); + } + catch (Exception ex) + { + throw new FrameworkException(ex); + } + } + + private void FailWithInvalidOperationException(object sender, ViewRegisteredEventArgs e) + { + throw new InvalidOperationException("Dont do this"); + } + + private class MockContentObject + { + } + + private class MySubscriberClass + { + public ViewRegisteredEventArgs onViewRegisteredArguments; + public void OnContentRegistered(object sender, ViewRegisteredEventArgs e) + { + onViewRegisteredArguments = e; + } + } + + private class FrameworkException : Exception + { + public FrameworkException(Exception innerException) + : base("", innerException) + { + + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/SelectorRegionAdapterFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/SelectorRegionAdapterFixture.cs new file mode 100644 index 0000000000..d6b38fd2bb --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/SelectorRegionAdapterFixture.cs @@ -0,0 +1,113 @@ +// TODO: 2022-07-13 +// Feature, SelectorRegionAdapter, is currently disabled. +/* +using System; +using Avalonia.Controls; +using Moq; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class SelectorRegionAdapterFixture + { + [StaFact] + public void AdapterAddsSelectorItemsSourceSyncBehavior() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var control = new ListBox(); + IRegionAdapter adapter = new TestableSelectorRegionAdapter(); + + IRegion region = adapter.Initialize(control, "Region1"); + Assert.NotNull(region); + + Assert.IsType(region.Behaviors["SelectorItemsSourceSyncBehavior"]); + } + + [StaFact] + public async Task AdapterDoesNotPreventRegionFromBeingGarbageCollected() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var selector = new ListBox(); + object model = new object(); + IRegionAdapter adapter = new SelectorRegionAdapter(null); + + var region = adapter.Initialize(selector, "Region1"); + region.Add(model); + + var regionWeakReference = new WeakReference(region); + var controlWeakReference = new WeakReference(selector); + Assert.True(regionWeakReference.IsAlive); + Assert.True(controlWeakReference.IsAlive); + + region = null; + selector = null; + await Task.Delay(50); + GC.Collect(); + GC.Collect(); + + Assert.False(regionWeakReference.IsAlive); + Assert.False(controlWeakReference.IsAlive); + } + + [StaFact] + public void ActivatingTheViewShouldUpdateTheSelectedItem() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var selector = new ListBox(); + var view1 = new object(); + var view2 = new object(); + + IRegionAdapter adapter = new SelectorRegionAdapter(null); + + var region = adapter.Initialize(selector, "Region1"); + region.Add(view1); + region.Add(view2); + + Assert.NotEqual(view1, selector.SelectedItem); + + region.Activate(view1); + + Assert.Equal(view1, selector.SelectedItem); + + region.Activate(view2); + + Assert.Equal(view2, selector.SelectedItem); + } + + [StaFact] + public void DeactivatingTheSelectedViewShouldUpdateTheSelectedItem() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + var selector = new ListBox(); + var view1 = new object(); + IRegionAdapter adapter = new SelectorRegionAdapter(null); + var region = adapter.Initialize(selector, "Region1"); + region.Add(view1); + + region.Activate(view1); + + Assert.Equal(view1, selector.SelectedItem); + + region.Deactivate(view1); + + Assert.NotEqual(view1, selector.SelectedItem); + } + + private class TestableSelectorRegionAdapter : SelectorRegionAdapter + { + public TestableSelectorRegionAdapter() + : base(null) + { + } + + + protected override IRegion CreateRegion() + { + return new Region(); + } + } + } +} +*/ diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/SingleActiveRegionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/SingleActiveRegionFixture.cs new file mode 100644 index 0000000000..0436fef4b8 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/SingleActiveRegionFixture.cs @@ -0,0 +1,29 @@ +using Moq; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class SingleActiveRegionFixture + { + [Fact] + public void ActivatingNewViewDeactivatesCurrent() + { + ContainerLocator.SetContainerExtension(Mock.Of()); + IRegion region = new SingleActiveRegion(); + var view = new object(); + region.Add(view); + region.Activate(view); + + Assert.True(region.ActiveViews.Contains(view)); + + var view2 = new object(); + region.Add(view2); + region.Activate(view2); + + Assert.False(region.ActiveViews.Contains(view)); + Assert.True(region.ActiveViews.Contains(view2)); + } + } +} diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/ViewsCollectionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ViewsCollectionFixture.cs new file mode 100644 index 0000000000..217e639a5b --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ViewsCollectionFixture.cs @@ -0,0 +1,346 @@ +// NOTE: +// Avalonia.Data.CollectionViewSource control does not exist in Avalonia. +// This feature was apart of a legacy build: +// https://github.com/grokys/Avalonia/blob/master/Avalonia/Data/CollectionViewSource.cs +// Avalonia PR #14729 in draft as of 2024-04-11 +// https://github.com/AvaloniaUI/Avalonia/pull/14729 +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions +{ + public class ViewsCollectionFixture + { + [Fact] + public void CanWrapCollectionCollection() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => true); + + Assert.Empty(viewsCollection); + + var item = new object(); + originalCollection.Add(new ItemMetadata(item)); + Assert.Single(viewsCollection); + Assert.Same(item, viewsCollection.First()); + } + + [Fact] + public void CanFilterCollection() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => x.Name == "Possible"); + + originalCollection.Add(new ItemMetadata(new object())); + + Assert.Empty(viewsCollection); + + var item = new object(); + originalCollection.Add(new ItemMetadata(item) { Name = "Possible" }); + Assert.Single(viewsCollection); + + Assert.Same(item, viewsCollection.First()); + } + + [Fact] + public void RaisesCollectionChangedWhenFilteredCollectionChanges() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => x.IsActive); + bool collectionChanged = false; + viewsCollection.CollectionChanged += (s, e) => collectionChanged = true; + + originalCollection.Add(new ItemMetadata(new object()) { IsActive = true }); + + Assert.True(collectionChanged); + } + + [Fact] + public void RaisesCollectionChangedWithAddAndRemoveWhenFilteredCollectionChanges() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => x.IsActive); + bool addedToCollection = false; + bool removedFromCollection = false; + viewsCollection.CollectionChanged += (s, e) => + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + addedToCollection = true; + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + removedFromCollection = true; + } + }; + var filteredInObject = new ItemMetadata(new object()) { IsActive = true }; + + originalCollection.Add(filteredInObject); + + Assert.True(addedToCollection); + Assert.False(removedFromCollection); + + originalCollection.Remove(filteredInObject); + + Assert.True(removedFromCollection); + } + + [Fact] + public void DoesNotRaiseCollectionChangedWhenAddingOrRemovingFilteredOutObject() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => x.IsActive); + bool collectionChanged = false; + viewsCollection.CollectionChanged += (s, e) => collectionChanged = true; + var filteredOutObject = new ItemMetadata(new object()) { IsActive = false }; + + originalCollection.Add(filteredOutObject); + originalCollection.Remove(filteredOutObject); + + Assert.False(collectionChanged); + } + + [Fact] + public void CollectionChangedPassesWrappedItemInArgumentsWhenAdding() + { + var originalCollection = new ObservableCollection(); + var filteredInObject = new ItemMetadata(new object()); + originalCollection.Add(filteredInObject); + + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => true); + IList oldItemsPassed = null; + viewsCollection.CollectionChanged += (s, e) => { oldItemsPassed = e.OldItems; }; + originalCollection.Remove(filteredInObject); + + Assert.NotNull(oldItemsPassed); + Assert.Single(oldItemsPassed); + Assert.Same(filteredInObject.Item, oldItemsPassed[0]); + } + + [Fact] + public void CollectionChangedPassesWrappedItemInArgumentsWhenRemoving() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => true); + IList newItemsPassed = null; + viewsCollection.CollectionChanged += (s, e) => { newItemsPassed = e.NewItems; }; + var filteredInObject = new ItemMetadata(new object()); + + originalCollection.Add(filteredInObject); + + Assert.NotNull(newItemsPassed); + Assert.Single(newItemsPassed); + Assert.Same(filteredInObject.Item, newItemsPassed[0]); + } + + [Fact] + public void EnumeratesWrappedItems() + { + var originalCollection = new ObservableCollection() + { + new ItemMetadata(new object()), + new ItemMetadata(new object()) + }; + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => true); + Assert.Equal(2, viewsCollection.Count()); + + Assert.Same(originalCollection[0].Item, viewsCollection.ElementAt(0)); + Assert.Same(originalCollection[1].Item, viewsCollection.ElementAt(1)); + } + + [Fact] + public void ChangingMetadataOnItemAddsOrRemovesItFromTheFilteredCollection() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, x => x.IsActive); + bool addedToCollection = false; + bool removedFromCollection = false; + viewsCollection.CollectionChanged += (s, e) => + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + addedToCollection = true; + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + removedFromCollection = true; + } + }; + + originalCollection.Add(new ItemMetadata(new object()) { IsActive = true }); + Assert.True(addedToCollection); + Assert.False(removedFromCollection); + addedToCollection = false; + + originalCollection[0].IsActive = false; + + Assert.Empty(viewsCollection); + Assert.True(removedFromCollection); + Assert.False(addedToCollection); + Assert.Empty(viewsCollection); + addedToCollection = false; + removedFromCollection = false; + + originalCollection[0].IsActive = true; + + Assert.Single(viewsCollection); + Assert.True(addedToCollection); + Assert.False(removedFromCollection); + } + + [Fact] + public void AddingToOriginalCollectionFiresAddCollectionChangeEvent() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => true); + + var eventTracker = new CollectionChangedTracker(viewsCollection); + + originalCollection.Add(new ItemMetadata(new object())); + + Assert.Contains(NotifyCollectionChangedAction.Add, eventTracker.ActionsFired); + } + + [Fact] + public void AddingToOriginalCollectionFiresResetNotificationIfSortComparisonSet() + { + // Reset is fired to support the need to resort after updating the collection + var originalCollection = new ObservableCollection(); + var viewsCollection = new ViewsCollection(originalCollection, (i) => true); + viewsCollection.SortComparison = (a, b) => { return 0; }; + + var eventTracker = new CollectionChangedTracker(viewsCollection); + + originalCollection.Add(new ItemMetadata(new object())); + + Assert.Contains(NotifyCollectionChangedAction.Add, eventTracker.ActionsFired); + Assert.Equal( + 1, + eventTracker.ActionsFired.Count(a => a == NotifyCollectionChangedAction.Reset)); + } + + [Fact] + public void OnAddNotifyCollectionChangedThenIndexProvided() + { + var originalCollection = new ObservableCollection(); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => true); + + var eventTracker = new CollectionChangedTracker(viewsCollection); + + originalCollection.Add(new ItemMetadata("a")); + + var addEvent = eventTracker.NotifyEvents.Single(e => e.Action == NotifyCollectionChangedAction.Add); + Assert.Equal(0, addEvent.NewStartingIndex); + } + + [Fact] + public void OnRemoveNotifyCollectionChangedThenIndexProvided() + { + var originalCollection = new ObservableCollection(); + originalCollection.Add(new ItemMetadata("a")); + originalCollection.Add(new ItemMetadata("b")); + originalCollection.Add(new ItemMetadata("c")); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => true); + + var eventTracker = new CollectionChangedTracker(viewsCollection); + originalCollection.RemoveAt(1); + + var removeEvent = eventTracker.NotifyEvents.Single(e => e.Action == NotifyCollectionChangedAction.Remove); + Assert.NotNull(removeEvent); + Assert.Equal(1, removeEvent.OldStartingIndex); + } + + [Fact] + public void OnRemoveOfFilterMatchingItemThenViewCollectionRelativeIndexProvided() + { + var originalCollection = new ObservableCollection(); + originalCollection.Add(new ItemMetadata("a")); + originalCollection.Add(new ItemMetadata("b")); + originalCollection.Add(new ItemMetadata("c")); + IViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => !"b".Equals(i.Item)); + + var eventTracker = new CollectionChangedTracker(viewsCollection); + originalCollection.RemoveAt(2); + + var removeEvent = eventTracker.NotifyEvents.Single(e => e.Action == NotifyCollectionChangedAction.Remove); + Assert.NotNull(removeEvent); + Assert.Equal(1, removeEvent.OldStartingIndex); + } + + //// NOTE: Avalonia.Data.CollectionViewSource control does not exist in Avalonia. + ////[Fact] + ////public void RemovingFromFilteredCollectionDoesNotThrow() + ////{ + //// var originalCollection = new ObservableCollection(); + //// originalCollection.Add(new ItemMetadata("a")); + //// originalCollection.Add(new ItemMetadata("b")); + //// originalCollection.Add(new ItemMetadata("c")); + //// IViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => true); + //// + //// CollectionViewSource cvs = new CollectionViewSource { Source = viewsCollection }; + //// + //// var view = cvs.View; + //// //try + //// //{ + //// originalCollection.RemoveAt(1); + //// //} + //// //catch (Exception ex) + //// //{ + //// //Assert.Fail(ex.Message); + //// //} + ////} + + [Fact] + public void ViewsCollectionSortedAfterAddingItemToOriginalCollection() + { + var originalCollection = new ObservableCollection(); + ViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => true); + viewsCollection.SortComparison = Region.DefaultSortComparison; + + var view1 = new MockSortableView1(); + var view2 = new MockSortableView2(); + var view3 = new MockSortableView3(); + + originalCollection.Add(new ItemMetadata(view2)); + originalCollection.Add(new ItemMetadata(view3)); + originalCollection.Add(new ItemMetadata(view1)); + + Assert.Same(view1, viewsCollection.ElementAt(0)); + Assert.Same(view2, viewsCollection.ElementAt(1)); + Assert.Same(view3, viewsCollection.ElementAt(2)); + } + + [Fact] + public void ChangingSortComparisonCausesResortingOfCollection() + { + var originalCollection = new ObservableCollection(); + ViewsCollection viewsCollection = new ViewsCollection(originalCollection, (i) => true); + + var view1 = new MockSortableView1(); + var view2 = new MockSortableView2(); + var view3 = new MockSortableView3(); + + originalCollection.Add(new ItemMetadata(view2)); + originalCollection.Add(new ItemMetadata(view3)); + originalCollection.Add(new ItemMetadata(view1)); + + // ensure items are in original order + Assert.Same(view2, viewsCollection.ElementAt(0)); + Assert.Same(view3, viewsCollection.ElementAt(1)); + Assert.Same(view1, viewsCollection.ElementAt(2)); + + // change sort comparison + viewsCollection.SortComparison = Region.DefaultSortComparison; + + // ensure items are properly sorted + Assert.Same(view1, viewsCollection.ElementAt(0)); + Assert.Same(view2, viewsCollection.ElementAt(1)); + Assert.Same(view3, viewsCollection.ElementAt(2)); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Application/PrismApplicationFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Application/PrismApplicationFixture.cs new file mode 100644 index 0000000000..aab3bb5ae6 --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Application/PrismApplicationFixture.cs @@ -0,0 +1,7 @@ +namespace Prism.Container.Avalonia.Tests.Fixtures.Application +{ + public class PrismApplicationFixture + { + // TODO: Write tests for PrismApplication + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperFixture.cs new file mode 100644 index 0000000000..b039f8431b --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperFixture.cs @@ -0,0 +1,117 @@ +using System; +using Prism.Container.Avalonia.Mocks; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Navigation.Regions; +using Xunit; +using static Prism.Container.Avalonia.Tests.ContainerHelper; + +namespace Prism.Container.Avalonia.Tests.Bootstrapper +{ + [Collection(nameof(ContainerExtension))] + public class BootstrapperFixture + { + [StaFact] + public void ContainerDefaultsToNull() + { + var bootstrapper = new MockBootstrapper(); + var container = bootstrapper.ContainerExtension; + + Assert.Null(container); + } + + [StaFact] + public void CanCreateConcreteBootstrapper() + { + new MockBootstrapper(); + } + + [StaFact] + public void CreateContainerShouldInitializeContainer() + { + var bootstrapper = new MockBootstrapper(); + + var container = bootstrapper.CallCreateContainer(); + + Assert.NotNull(container); + Assert.IsAssignableFrom(BaseContainerInterfaceType, container); + } + + [StaFact] + public void ConfigureContainerAddsModuleCatalogToContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + var returnedCatalog = bootstrapper.ContainerExtension.Resolve(); + Assert.NotNull(returnedCatalog); + Assert.IsType(returnedCatalog); + } + + [StaFact] + public void ConfigureContainerAddsRegionNavigationJournalEntryToContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.ContainerExtension.Resolve(); + var actual2 = bootstrapper.ContainerExtension.Resolve(); + + Assert.NotNull(actual1); + Assert.NotNull(actual2); + Assert.NotSame(actual1, actual2); + } + + [StaFact] + public void ConfigureContainerAddsRegionNavigationJournalToContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.ContainerExtension.Resolve(); + var actual2 = bootstrapper.ContainerExtension.Resolve(); + + Assert.NotNull(actual1); + Assert.NotNull(actual2); + Assert.NotSame(actual1, actual2); + } + + [StaFact] + public void ConfigureContainerAddsRegionNavigationServiceToContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.ContainerExtension.Resolve(); + var actual2 = bootstrapper.ContainerExtension.Resolve(); + + Assert.NotNull(actual1); + Assert.NotNull(actual2); + Assert.NotSame(actual1, actual2); + } + + [StaFact] + public void ConfigureContainerAddsNavigationTargetHandlerToContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.ContainerExtension.Resolve(); + var actual2 = bootstrapper.ContainerExtension.Resolve(); + + Assert.NotNull(actual1); + Assert.NotNull(actual2); + Assert.Same(actual1, actual2); + } + + [StaFact] + public void RegisterFrameworkExceptionTypesShouldRegisterResolutionFailedException() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.CallRegisterFrameworkExceptionTypes(); + + Assert.True(ExceptionExtensions.IsFrameworkExceptionRegistered(RegisteredFrameworkException)); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperNullModuleCatalogFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperNullModuleCatalogFixture.cs new file mode 100644 index 0000000000..f3c25d7207 --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperNullModuleCatalogFixture.cs @@ -0,0 +1,18 @@ +using System; +using Prism.Container.Avalonia.Mocks; +using Prism.IocContainer.Avalonia.Tests.Support; +using Xunit; + +namespace Prism.Container.Avalonia.Tests.Bootstrapper +{ + public class BootstrapperNullModuleCatalogFixture : BootstrapperFixtureBase + { + [Fact] + public void NullModuleCatalogThrowsOnDefaultModuleInitialization() + { + var bootstrapper = new NullModuleCatalogBootstrapper(); + + AssertExceptionThrownOnRun(bootstrapper, typeof(InvalidOperationException), "IModuleCatalog"); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperRunMethodFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperRunMethodFixture.cs new file mode 100644 index 0000000000..32a4dda4f3 --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Bootstrapper/BootstrapperRunMethodFixture.cs @@ -0,0 +1,234 @@ +using Moq; +using Prism.Container.Avalonia.Mocks; +using Prism.Events; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Container.Avalonia.Tests.Bootstrapper +{ + [Collection(nameof(ContainerExtension))] + public partial class BootstrapperRunMethodFixture + { + [StaFact] + public void CanRunBootstrapper() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + } + + [StaFact] + public void RunShouldNotFailIfReturnedNullShell() + { + var bootstrapper = new MockBootstrapper { ShellObject = null }; + bootstrapper.Run(); + } + + [StaFact] + public void RunSetsCurrentContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + Assert.NotNull(ContainerLocator.Container); + Assert.IsType(ContainerHelper.ContainerExtensionType, ContainerLocator.Container); + } + + [StaFact] + public void RunShouldInitializeContainer() + { + var bootstrapper = new MockBootstrapper(); + var container = bootstrapper.BaseContainer; + + Assert.Null(container); + + bootstrapper.Run(); + + container = bootstrapper.BaseContainer; + + Assert.NotNull(container); + Assert.IsType(ContainerHelper.BaseContainerType, container); + } + + [StaFact] + public void RunShouldCallInitializeModules() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + Assert.True(bootstrapper.InitializeModulesCalled); + } + + [StaFact] + public void RunShouldCallConfigureDefaultRegionBehaviors() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.ConfigureDefaultRegionBehaviorsCalled); + } + + [StaFact] + public void RunShouldCallConfigureRegionAdapterMappings() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.ConfigureRegionAdapterMappingsCalled); + } + + [StaFact] + public void RunShouldAssignRegionManagerToReturnedShell() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.NotNull(RegionManager.GetRegionManager(bootstrapper.BaseShell)); + } + + [StaFact] + public void RunShouldCallCreateModuleCatalog() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.CreateModuleCatalogCalled); + } + + [StaFact] + public void RunShouldCallConfigureModuleCatalog() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.ConfigureModuleCatalogCalled); + } + + [StaFact] + public void RunShouldCallCreateContainer() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.CreateContainerCalled); + } + + [StaFact] + public void RunShouldCallCreateShell() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.CreateShellCalled); + } + + [StaFact] + public void RunShouldCallRegisterRequiredTypes() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.RegisterRequiredTypesCalled); + } + + [StaFact] + public void RunShouldCallRegisterTypes() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.RegisterTypesCalled); + } + + [StaFact] + public void SetsContainerLocatorCurrentContainer() + { + ContainerLocator.ResetContainer(); + Assert.False(ContainerLocator.IsInitialized); + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.NotNull(ContainerLocator.Container); + Assert.Same(bootstrapper.Container, ContainerLocator.Container); + } + + [StaFact] + public void RunShouldCallConfigureViewModelLocator() + { + var bootstrapper = new MockBootstrapper(); + + bootstrapper.Run(); + + Assert.True(bootstrapper.ConfigureViewModelLocatorCalled); + } + + [StaFact] + public void ModuleManagerRunCalled() + { + // Have to use a non-mocked container because of IsRegistered<> extension method, Registrations property,and ContainerRegistration + var mockedModuleInitializer = new Mock(); + var mockedModuleManager = new Mock(); + var regionAdapterMappings = new RegionAdapterMappings(); + var container = ContainerHelper.CreateContainerExtension(); + var regionBehaviorFactory = new RegionBehaviorFactory(container); + + container.RegisterInstance(container); + container.RegisterInstance(new ModuleCatalog()); + container.RegisterInstance(mockedModuleInitializer.Object); + container.RegisterInstance(mockedModuleManager.Object); + container.RegisterInstance(regionAdapterMappings); + + container.RegisterSingleton(typeof(RegionAdapterMappings), typeof(RegionAdapterMappings)); + container.RegisterSingleton(typeof(IRegionManager), typeof(RegionManager)); + container.RegisterSingleton(typeof(IEventAggregator), typeof(EventAggregator)); + container.RegisterSingleton(typeof(IRegionViewRegistry), typeof(RegionViewRegistry)); + container.RegisterSingleton(typeof(IRegionBehaviorFactory), typeof(RegionBehaviorFactory)); + container.RegisterSingleton(typeof(IRegionNavigationJournalEntry), typeof(RegionNavigationJournalEntry)); + container.RegisterSingleton(typeof(IRegionNavigationJournal), typeof(RegionNavigationJournal)); + container.RegisterSingleton(typeof(IRegionNavigationService), typeof(RegionNavigationService)); + container.RegisterSingleton(typeof(IRegionNavigationContentLoader), typeof(RegionNavigationContentLoader)); + + // TODO: container.RegisterInstance(new SelectorRegionAdapter(regionBehaviorFactory)); + container.RegisterInstance(new ItemsControlRegionAdapter(regionBehaviorFactory)); + container.RegisterInstance(new ContentControlRegionAdapter(regionBehaviorFactory)); + + var bootstrapper = new MockedContainerBootstrapper(container.GetBaseContainer()); + bootstrapper.Run(false); + + mockedModuleManager.Verify(mm => mm.Run(), Times.Once()); + } + + [StaFact] + public void RunShouldCallTheMethodsInOrder() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + var index = 0; + Assert.Equal("ConfigureViewModelLocator", bootstrapper.MethodCalls[index++]); + Assert.Equal("CreateContainerExtension", bootstrapper.MethodCalls[index++]); + Assert.Equal("CreateModuleCatalog", bootstrapper.MethodCalls[index++]); + Assert.Equal("RegisterRequiredTypes", bootstrapper.MethodCalls[index++]); + Assert.Equal("RegisterTypes", bootstrapper.MethodCalls[index++]); + Assert.Equal("ConfigureModuleCatalog", bootstrapper.MethodCalls[index++]); + Assert.Equal("ConfigureRegionAdapterMappings", bootstrapper.MethodCalls[index++]); + Assert.Equal("ConfigureDefaultRegionBehaviors", bootstrapper.MethodCalls[index++]); + Assert.Equal("RegisterFrameworkExceptionTypes", bootstrapper.MethodCalls[index++]); + Assert.Equal("CreateShell", bootstrapper.MethodCalls[index++]); + Assert.Equal("InitializeShell", bootstrapper.MethodCalls[index++]); + Assert.Equal("InitializeModules", bootstrapper.MethodCalls[index++]); + Assert.Equal("OnInitialized", bootstrapper.MethodCalls[index++]); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/ContainerExtensionCollection.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/ContainerExtensionCollection.cs new file mode 100644 index 0000000000..1b84488a8a --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/ContainerExtensionCollection.cs @@ -0,0 +1,11 @@ +using Xunit; + +namespace Prism.Container.Avalonia.Tests +{ + public class ContainerExtension { } + + [CollectionDefinition(nameof(ContainerExtension), DisableParallelization = true)] + public class ContainerExtensionCollection : ICollectionFixture + { + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Ioc/ContainerExtensionFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Ioc/ContainerExtensionFixture.cs new file mode 100644 index 0000000000..08d9538e5b --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Ioc/ContainerExtensionFixture.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Prism.Ioc; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks; +using Xunit; +using static Prism.Container.Avalonia.Tests.ContainerHelper; + +namespace Prism.Container.Avalonia.Tests.Ioc +{ + public class ContainerExtensionFixture + { + [Fact] + public void ExtensionReturnsTrueIfThereIsAPolicyForType() + { + var container = CreateContainerExtension(); + + container.Register(); + Assert.True(container.IsRegistered(typeof(object))); + Assert.False(container.IsRegistered(typeof(int))); + + container.Register, List>(); + + Assert.True(container.IsRegistered(typeof(IList))); + Assert.False(container.IsRegistered(typeof(IList))); + + container.Register(typeof(IDictionary<,>), typeof(Dictionary<,>)); + Assert.True(container.IsRegistered(typeof(IDictionary<,>))); + } + + [Fact] + public void TryResolveShouldResolveTheElementIfElementExist() + { + var container = CreateContainerExtension(); + container.Register(); + + object dependantA = null; + var ex = Record.Exception(() => dependantA = container.Resolve()); + Assert.Null(ex); + Assert.NotNull(dependantA); + } + + [Fact] + public void TryResolveShouldReturnNullIfElementNotExist() + { + var container = CreateContainerExtension(); + + object dependantA = null; + var ex = Record.Exception(() => dependantA = container.Resolve()); + Assert.NotNull(ex); + Assert.Null(dependantA); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Ioc/ContainerProviderExtensionFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Ioc/ContainerProviderExtensionFixture.cs new file mode 100644 index 0000000000..d37fb1de95 --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Ioc/ContainerProviderExtensionFixture.cs @@ -0,0 +1,162 @@ +/* + * TODO: Fix me for Avalonia +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Prism.Ioc; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks; +using Xunit; + +namespace Prism.Container.Avalonia.Tests.Ioc +{ + public class ContainerProviderExtensionFixture : IDisposable + { + private static readonly MockService _unnamedService = new MockService(); + private static readonly IReadOnlyDictionary _namedServiceDictionary = new Dictionary + { + ["A"] = new MockService(), + ["B"] = new MockService(), + }; + + private static readonly IContainerExtension _containerExtension + = ContainerHelper.CreateContainerExtension(); + + static ContainerProviderExtensionFixture() + { + // Preload assembly to resolve 'xmlns:prism' on xaml. + Assembly.Load("Prism.Avalonia"); + + _containerExtension.RegisterInstance(_unnamedService); + foreach (var kvp in _namedServiceDictionary) + { + _containerExtension.RegisterInstance(kvp.Value, kvp.Key); + } + } + + public ContainerProviderExtensionFixture() + { + ContainerLocator.ResetContainer(); + ContainerLocator.SetContainerExtension(_containerExtension); + } + + public void Dispose() + { + ContainerLocator.ResetContainer(); + } + + [Fact] + public void CanResolveUnnamedServiceUsingConstructor() + { + var containerProvider = new ContainerProviderExtension(typeof(IService)); + var service = containerProvider.ProvideValue(null); + + Assert.Same(_unnamedService, service); + } + + [Fact] + public void CanResolveUnnamedServiceUsingProperty() + { + var containerProvider = new ContainerProviderExtension + { + Type = typeof(IService) + }; + var service = containerProvider.ProvideValue(null); + + Assert.Same(_unnamedService, service); + } + + [Theory] + [InlineData("A")] + [InlineData("B")] + public void CanResolvedNamedServiceUsingConstructor(string name) + { + var expectedService = _namedServiceDictionary[name]; + + var containerProvider = new ContainerProviderExtension(typeof(IService)) + { + Name = name, + }; + var service = containerProvider.ProvideValue(null); + + Assert.Same(expectedService, service); + } + + [Theory] + [InlineData("A")] + [InlineData("B")] + public void CanResolvedNamedServiceUsingProperty(string name) + { + var expectedService = _namedServiceDictionary[name]; + + var containerProvider = new ContainerProviderExtension() + { + Type = typeof(IService), + Name = name, + }; + var service = containerProvider.ProvideValue(null); + + Assert.Same(expectedService, service); + } + + private const string _xamlWithMarkupExtension = +@""; + + private const string _xamlWithXmlElement = +@" + + + +"; + + [Theory] + [InlineData(_xamlWithMarkupExtension)] + [InlineData(_xamlWithXmlElement)] + public void CanResolveServiceFromXaml(string xaml) + { + // Don't use StaTheoryAttribute. + // If use StaTheoryAttribute, ContainerLocator.Current will be changed by other test method + // and Window.DataContext will be null. + + object dataContext = null; + try + { + var thread = new Thread(() => + { + ////using (var reader = new StringReader(xaml)) + ////{ + //// var window = XamlServices.Load(reader) as Window; + //// dataContext = window.DataContext; + ////} + + var window = AvaloniaRuntimeXamlLoader.Load(xaml) as Window; + dataContext = window.DataContext; + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + } + catch (Exception ex) + { + Console.WriteLine("Issue resolving AXAML: " + ex); + Debug.WriteLine("Issue resolving AXAML: " + ex); + } + + Assert.Same(_unnamedService, dataContext); + } + } +} +*/ diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Mvvm/ViewModelLocatorFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Mvvm/ViewModelLocatorFixture.cs new file mode 100644 index 0000000000..e88210ec9b --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Mvvm/ViewModelLocatorFixture.cs @@ -0,0 +1,32 @@ +using Prism.Container.Avalonia.Mocks; +using Prism.Ioc; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks.ViewModels; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks.Views; +using Prism.Mvvm; +using Xunit; + +namespace Prism.Container.Avalonia.Tests.Mvvm +{ + [Collection(nameof(ContainerExtension))] + public class ViewModelLocatorFixture + { + [StaFact] + public void ShouldLocateViewModelAndResolveWithContainer() + { + var bootstrapper = new MockBootstrapper(); + bootstrapper.Run(); + + bootstrapper.ContainerRegistry.Register(); + + var view = new MockView(); + Assert.Null(view.DataContext); + + ViewModelLocator.SetAutoWireViewModel(view, true); + Assert.NotNull(view.DataContext); + Assert.IsType(view.DataContext); + + Assert.NotNull(((MockViewModel)view.DataContext).MockService); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Regions/RegionNavigationContentLoaderFixture.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Regions/RegionNavigationContentLoaderFixture.cs new file mode 100644 index 0000000000..865ac55d3f --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Fixtures/Regions/RegionNavigationContentLoaderFixture.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Prism.Ioc; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks.Views; +using Prism.Navigation.Regions; +using Xunit; +using static Prism.Container.Avalonia.Tests.ContainerHelper; + +namespace Prism.Container.Avalonia.Tests.Regions +{ + [Collection(nameof(ContainerExtension))] + public class RegionNavigationContentLoaderFixture + { + readonly IContainerExtension _container; + + public RegionNavigationContentLoaderFixture() + { + _container = CreateContainerExtension(); + _container.RegisterInstance(_container); + _container.Register(); + _container.Register(typeof(IRegionNavigationContentLoader), typeof(RegionNavigationContentLoader)); + _container.Register(); + ContainerLocator.ResetContainer(); + ContainerLocator.SetContainerExtension(_container); + } + + [StaFact] + public void ShouldFindCandidateViewInRegion() + { + _container.RegisterForNavigation(); + //_container.RegisterType("MockView"); + + // We cannot access the UnityRegionNavigationContentLoader directly so we need to call its + // GetCandidatesFromRegion method through a navigation request. + IRegion testRegion = new Region(); + + MockView view = new MockView(); + testRegion.Add(view); + testRegion.Deactivate(view); + + Assert.True(((IContainerRegistry)_container).IsRegistered("MockView")); + + testRegion.RequestNavigate("MockView"); + + Assert.Contains(view, testRegion.Views); + Assert.Single(testRegion.Views); + Assert.Single(testRegion.ActiveViews); + Assert.Contains(view, testRegion.ActiveViews); + } + + [StaFact] + public void ShouldFindCandidateViewWithFriendlyNameInRegion() + { + _container.RegisterForNavigation("SomeView"); + + // We cannot access the Container specific RegionNavigationContentLoader directly so we need to call its + // GetCandidatesFromRegion method through a navigation request. + IRegion testRegion = new Region(); + + var view = _container.Resolve("SomeView") as MockView; + testRegion.Add(view); + testRegion.Deactivate(view); + + Assert.Empty(testRegion.ActiveViews); + testRegion.RequestNavigate("SomeView"); + + Assert.Contains(view, testRegion.Views); + Assert.Single(testRegion.Views); + Assert.Single(testRegion.ActiveViews); + Assert.Contains(view, testRegion.ActiveViews); + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Mocks/NullModuleCatalogBootstrapper.cs b/tests/Avalonia/Prism.Container.Avalonia.Shared/Mocks/NullModuleCatalogBootstrapper.cs new file mode 100644 index 0000000000..48a246c9fd --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Mocks/NullModuleCatalogBootstrapper.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Prism.Ioc; +using Prism.Modularity; + +namespace Prism.Container.Avalonia.Mocks +{ + internal partial class NullModuleCatalogBootstrapper + { + protected override IModuleCatalog CreateModuleCatalog() + { + return null; + } + + protected override AvaloniaObject CreateShell() + { + return null; + } + + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + } +} diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Prism.Container.Avalonia.Shared.projitems b/tests/Avalonia/Prism.Container.Avalonia.Shared/Prism.Container.Avalonia.Shared.projitems new file mode 100644 index 0000000000..86e2cb6513 --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Prism.Container.Avalonia.Shared.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + bd42a7d6-a84d-4d27-9c28-7f6a2ec477f1 + + + Prism.Container.Wpf.Tests + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia/Prism.Container.Avalonia.Shared/Prism.Container.Avalonia.Shared.shproj b/tests/Avalonia/Prism.Container.Avalonia.Shared/Prism.Container.Avalonia.Shared.shproj new file mode 100644 index 0000000000..8960a160e7 --- /dev/null +++ b/tests/Avalonia/Prism.Container.Avalonia.Shared/Prism.Container.Avalonia.Shared.shproj @@ -0,0 +1,13 @@ + + + + {6FDA7D49-DF44-4E8F-A182-0EB73DD3C452} + 17.0 + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/ContainerHelper.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/ContainerHelper.cs new file mode 100644 index 0000000000..1a46203012 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/ContainerHelper.cs @@ -0,0 +1,34 @@ +using System; +using DryIoc; +using Prism.Container.DryIoc; +using Prism.Ioc; + +namespace Prism.Container.Avalonia.Tests +{ + public static class ContainerHelper + { + private static Rules CreateContainerRules() => Rules.Default.WithAutoConcreteTypeResolution() + .With(Made.Of(FactoryMethod.ConstructorWithResolvableArguments)) + .WithDefaultIfAlreadyRegistered(IfAlreadyRegistered.Replace); + + public static IContainer CreateContainer() => + new global::DryIoc.Container(CreateContainerRules()); + + public static IContainerExtension CreateContainerExtension() => + new DryIocContainerExtension(CreateContainer()); + + public static IContainer GetBaseContainer(this IContainerExtension container) => + ((IContainerProvider)container).GetContainer(); + + public static IContainer GetBaseContainer(this IContainerProvider container) => + container.GetContainer(); + + public static Type ContainerExtensionType => typeof(DryIocContainerExtension); + + public static Type BaseContainerType => typeof(global::DryIoc.Container); + + public static Type BaseContainerInterfaceType = typeof(IContainer); + + public static Type RegisteredFrameworkException = typeof(ContainerException); + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Fixtures/BootstrapperRunMethodFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Fixtures/BootstrapperRunMethodFixture.cs new file mode 100644 index 0000000000..09c49373fc --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Fixtures/BootstrapperRunMethodFixture.cs @@ -0,0 +1,169 @@ +using System; +using DryIoc; +using Moq; +using Prism.Container.DryIoc; +using Prism.Container.Avalonia.Mocks; +using Prism.DryIoc; +using Prism.Events; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Container.Avalonia.Tests.Bootstrapper +{ + public partial class BootstrapperRunMethodFixture + { + [StaFact] + public void RunAddsCompositionContainerToContainer() + { + var bootstrapper = new MockBootstrapper(); + + var createdContainer = bootstrapper.CallCreateContainer(); + var returnedContainer = createdContainer.Resolve(); + Assert.NotNull(returnedContainer); + Assert.Equal(typeof(global::DryIoc.Container), returnedContainer.GetType()); + } + + [StaFact] + public void RunRegistersInstanceOfIModuleCatalog() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IModuleCatalog), It.IsAny(), It.IsAny(), It.IsAny())); + } + + [StaFact] + public void RunRegistersTypeForIModuleInitializer() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IModuleInitializer), null, It.IsAny(), It.IsAny()), Times.Once()); + } + + [StaFact] + public void RunRegistersTypeForIRegionManager() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IRegionManager), null, It.IsAny(), It.IsAny()), Times.Once()); + } + + [StaFact] + public void RunRegistersTypeForRegionAdapterMappings() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(RegionAdapterMappings), null, It.IsAny(), It.IsAny()), Times.Once()); + } + + [StaFact] + public void RunRegistersTypeForIRegionViewRegistry() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IRegionViewRegistry), null, It.IsAny(), It.IsAny()), Times.Once()); + } + + [StaFact] + public void RunRegistersTypeForIRegionBehaviorFactory() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IRegionBehaviorFactory), null, It.IsAny(), It.IsAny()), Times.Once()); + } + + [StaFact] + public void RunRegistersTypeForIEventAggregator() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + + bootstrapper.Run(); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IEventAggregator), null, It.IsAny(), It.IsAny()), Times.Once()); + } + + [StaFact] + public void RunFalseShouldNotRegisterDefaultServicesAndTypes() + { + var mockedContainer = new Mock(); + SetupMockedContainerForVerificationTests(mockedContainer); + + var bootstrapper = new MockedContainerBootstrapper(mockedContainer.Object); + bootstrapper.Run(false); + + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IEventAggregator), null, It.IsAny(), It.IsAny()), Times.Never()); + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IRegionManager), null, It.IsAny(), It.IsAny()), Times.Never()); + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(RegionAdapterMappings), null, It.IsAny(), It.IsAny()), Times.Never()); + mockedContainer.Verify(c => c.Register(It.IsAny(), typeof(IModuleInitializer), null, It.IsAny(), It.IsAny()), Times.Never()); + } + + private static void SetupMockedContainerForVerificationTests(Mock mockedContainer) + { + var mockedModuleInitializer = new Mock(); + var mockedModuleManager = new Mock(); + var regionAdapterMappings = new RegionAdapterMappings(); + + var containerExtension = new DryIocContainerExtension(mockedContainer.Object); + var regionBehaviorFactory = new RegionBehaviorFactory(containerExtension); + + mockedContainer.Setup(c => c.Register(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + + // NOTE: The actual method called by Prism's DryIocContainerExtension is off over the IResolver not IContainer + mockedContainer.As().Setup(r => r.Resolve(typeof(IModuleCatalog), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + new ModuleCatalog()); + + mockedContainer.As().Setup(c => c.Resolve(typeof(IModuleInitializer), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + mockedModuleInitializer.Object); + + mockedContainer.As().Setup(c => c.Resolve(typeof(IModuleManager), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + mockedModuleManager.Object); + + mockedContainer.As().Setup(c => c.Resolve(typeof(RegionAdapterMappings), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + regionAdapterMappings); + + // TODO: Implement SelectorRegion + /////mockedContainer.As().Setup(c => c.Resolve(typeof(SelectorRegionAdapter), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + ///// new SelectorRegionAdapter(regionBehaviorFactory)); + + mockedContainer.As().Setup(c => c.Resolve(typeof(ItemsControlRegionAdapter), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + new ItemsControlRegionAdapter(regionBehaviorFactory)); + + mockedContainer.As().Setup(c => c.Resolve(typeof(ContentControlRegionAdapter), It.IsAny(), IfUnresolved.Throw, It.IsAny(), It.IsAny(), It.IsAny())).Returns( + new ContentControlRegionAdapter(regionBehaviorFactory)); + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/MockBootstrapper.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/MockBootstrapper.cs new file mode 100644 index 0000000000..f6bbc5b161 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/MockBootstrapper.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using DryIoc; +using Prism.Container.DryIoc; +using Prism.DryIoc; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Navigation.Regions; + +namespace Prism.Container.Avalonia.Mocks +{ + internal class MockBootstrapper : PrismBootstrapper + { + public List MethodCalls = new List(); + public bool InitializeModulesCalled; + public bool ConfigureRegionAdapterMappingsCalled; + public RegionAdapterMappings DefaultRegionAdapterMappings; + public bool CreateModuleCatalogCalled; + public bool RegisterRequiredTypesCalled; + public bool RegisterTypesCalled; + public bool CreateShellCalled; + public bool CreateContainerCalled; + public bool ConfigureModuleCatalogCalled; + public bool InitializeShellCalled; + public bool OnInitializeCalled; + public bool ConfigureViewModelLocatorCalled; + public bool ConfigureDefaultRegionBehaviorsCalled; + public UserControl ShellObject = new UserControl(); + + public AvaloniaObject BaseShell => base.Shell; + + public IContainer BaseContainer + { + get => base.Container?.GetContainer(); + } + + public IContainerExtension ContainerExtension => (IContainerExtension)base.Container; + + public IContainerRegistry ContainerRegistry => (IContainerRegistry)base.Container; + + public IContainer CallCreateContainer() + { + var containerExt = this.CreateContainerExtension(); + return ((IContainerExtension)containerExt).Instance; + } + + protected override AvaloniaObject CreateShell() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.CreateShellCalled = true; + return ShellObject; + } + + protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry) + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.RegisterRequiredTypesCalled = true; + base.RegisterRequiredTypes(containerRegistry); + } + + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.RegisterTypesCalled = true; + } + + protected override void Initialize() + { + ContainerLocator.ResetContainer(); + base.Initialize(); + } + + protected override IContainerExtension CreateContainerExtension() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.CreateContainerCalled = true; + return base.CreateContainerExtension(); + } + + protected override void ConfigureViewModelLocator() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.ConfigureViewModelLocatorCalled = true; + base.ConfigureViewModelLocator(); + } + + protected override IModuleCatalog CreateModuleCatalog() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.CreateModuleCatalogCalled = true; + return base.CreateModuleCatalog(); + } + + protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.ConfigureModuleCatalogCalled = true; + base.ConfigureModuleCatalog(moduleCatalog); + } + + protected override void InitializeShell(AvaloniaObject shell) + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.InitializeShellCalled = true; + base.InitializeShell(shell); + } + + protected override void OnInitialized() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.OnInitializeCalled = true; + base.OnInitialized(); + } + + protected override void InitializeModules() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.InitializeModulesCalled = true; + base.InitializeModules(); + } + + protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + this.ConfigureDefaultRegionBehaviorsCalled = true; + base.ConfigureDefaultRegionBehaviors(regionBehaviors); + } + + protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + ConfigureRegionAdapterMappingsCalled = true; + + base.ConfigureRegionAdapterMappings(regionAdapterMappings); + + DefaultRegionAdapterMappings = regionAdapterMappings; + } + + protected override void RegisterFrameworkExceptionTypes() + { + this.MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + base.RegisterFrameworkExceptionTypes(); + } + + public void CallRegisterFrameworkExceptionTypes() + { + base.RegisterFrameworkExceptionTypes(); + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/MockedContainerBootstrapper.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/MockedContainerBootstrapper.cs new file mode 100644 index 0000000000..ac72e02831 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/MockedContainerBootstrapper.cs @@ -0,0 +1,55 @@ +using Avalonia; +using Avalonia.Controls; +using DryIoc; +using Prism.Container.DryIoc; +using Prism.DryIoc; +using Prism.Ioc; + +namespace Prism.Container.Avalonia.Mocks +{ + internal class MockedContainerBootstrapper : PrismBootstrapper + { + private readonly IContainer _container; + + public MockedContainerBootstrapper(IContainer container) + { + ContainerLocator.ResetContainer(); + this._container = container; + } + + bool _useDefaultConfiguration = true; + + public void Run(bool useDefaultConfiguration) + { + _useDefaultConfiguration = useDefaultConfiguration; + + base.Run(); + } + + protected override IContainerExtension CreateContainerExtension() + { + return new DryIocContainerExtension(_container); + } + + protected override AvaloniaObject CreateShell() + { + return new UserControl(); + } + + protected override void InitializeShell(AvaloniaObject shell) + { + + } + + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + + } + + protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry) + { + if (_useDefaultConfiguration) + base.RegisterRequiredTypes(containerRegistry); + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/NullLoggerBootstrapper.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/NullLoggerBootstrapper.cs new file mode 100644 index 0000000000..4cd0b8a06b --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/NullLoggerBootstrapper.cs @@ -0,0 +1,20 @@ +using System.Windows; +using Avalonia; +using Prism.DryIoc; +using Prism.Ioc; + +namespace Prism.Container.Avalonia.Mocks +{ + internal partial class NullLoggerBootstrapper : PrismBootstrapper + { + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + throw new System.NotImplementedException(); + } + + protected override AvaloniaObject CreateShell() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/NullModuleCatalogBootstrapper.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/NullModuleCatalogBootstrapper.cs new file mode 100644 index 0000000000..fb4df6e59e --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Mocks/NullModuleCatalogBootstrapper.cs @@ -0,0 +1,8 @@ +using Prism.DryIoc; + +namespace Prism.Container.Avalonia.Mocks +{ + internal partial class NullModuleCatalogBootstrapper : PrismBootstrapper + { + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Prism.DryIoc.Avalonia.Tests.csproj b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Prism.DryIoc.Avalonia.Tests.csproj new file mode 100644 index 0000000000..bccbbb065b --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Prism.DryIoc.Avalonia.Tests.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/BootstrapperFixtureBase.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/BootstrapperFixtureBase.cs new file mode 100644 index 0000000000..7cfb2224ca --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/BootstrapperFixtureBase.cs @@ -0,0 +1,28 @@ +using System; +using Xunit; + +namespace Prism.IocContainer.Avalonia.Tests.Support +{ + public class BootstrapperFixtureBase + { + protected static void AssertExceptionThrownOnRun(PrismBootstrapperBase bootstrapper, Type expectedExceptionType, string expectedExceptionMessageSubstring) + { + bool exceptionThrown = false; + try + { + bootstrapper.Run(); + } + catch (Exception ex) + { + Assert.Equal(expectedExceptionType, ex.GetType()); + Assert.Contains(expectedExceptionMessageSubstring, ex.Message); + exceptionThrown = true; + } + + if (!exceptionThrown) + { + //Assert.Fail("Exception not thrown."); + } + } + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/DependantA.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/DependantA.cs new file mode 100644 index 0000000000..9ae44350af --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/DependantA.cs @@ -0,0 +1,20 @@ + + + +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks +{ + public class DependantA : IDependantA + { + public DependantA(IDependantB dependantB) + { + MyDependantB = dependantB; + } + + public IDependantB MyDependantB { get; set; } + } + + public interface IDependantA + { + IDependantB MyDependantB { get; } + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/DependantB.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/DependantB.cs new file mode 100644 index 0000000000..d73f6518f0 --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/DependantB.cs @@ -0,0 +1,20 @@ + + + +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks +{ + public class DependantB : IDependantB + { + public DependantB(IService service) + { + MyService = service; + } + + public IService MyService { get; set; } + } + + public interface IDependantB + { + IService MyService { get; } + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockModuleLoader.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockModuleLoader.cs new file mode 100644 index 0000000000..ff4fe78449 --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockModuleLoader.cs @@ -0,0 +1,14 @@ +using Prism.Modularity; + +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks +{ + public class MockModuleInitializer : IModuleInitializer + { + public bool LoadCalled; + + public void Initialize(IModuleInfo moduleInfo) + { + LoadCalled = true; + } + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockRegionManager.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockRegionManager.cs new file mode 100644 index 0000000000..8838505827 --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockRegionManager.cs @@ -0,0 +1,99 @@ +using System; +using Prism.Ioc; +using Prism.Navigation; +using Prism.Navigation.Regions; + +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks +{ + public class MockRegionManager : IRegionManager + { + #region IRegionManager Members + + public IRegionCollection Regions + { + get { throw new NotImplementedException(); } + } + + public IRegionManager CreateRegionManager() + { + throw new NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, object view) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Type viewType) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source, Action navigationCallback) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string source) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, Action navigationCallback, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, Uri target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + public void RequestNavigate(string regionName, string target, INavigationParameters navigationParameters) + { + throw new NotImplementedException(); + } + + #endregion + + public bool Navigate(Uri source) + { + throw new NotImplementedException(); + } + + public IRegionManager AddToRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, string viewName) + { + throw new NotImplementedException(); + } + + public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockService.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockService.cs new file mode 100644 index 0000000000..3eb447a5b2 --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/MockService.cs @@ -0,0 +1,8 @@ +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks +{ + public class MockService : IService + { + } + + public interface IService { } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/ViewModels/MockViewModel.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/ViewModels/MockViewModel.cs new file mode 100644 index 0000000000..02bdb6843a --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/ViewModels/MockViewModel.cs @@ -0,0 +1,19 @@ +using Prism.Mvvm; + +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks.ViewModels +{ + public class MockViewModel : BindableBase + { + private IService _mockService; + public IService MockService + { + get { return _mockService; } + set { SetProperty(ref _mockService, value); } + } + + public MockViewModel(IService mockService) + { + _mockService = mockService; + } + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/Views/MockView.cs b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/Views/MockView.cs new file mode 100644 index 0000000000..2908c73eb2 --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Mocks/Views/MockView.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace Prism.IocContainer.Avalonia.Tests.Support.Mocks.Views +{ + public class MockView : Control + { + } +} diff --git a/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Prism.IocContainer.Avalonia.Tests.Support.csproj b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Prism.IocContainer.Avalonia.Tests.Support.csproj new file mode 100644 index 0000000000..7040cf1449 --- /dev/null +++ b/tests/Avalonia/Prism.IocContainer.Avalonia.Tests.Support/Prism.IocContainer.Avalonia.Tests.Support.csproj @@ -0,0 +1,34 @@ + + + + + net8.0 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + +