From 015a06760789d910ebd00797ca0e4ef37f1f4834 Mon Sep 17 00:00:00 2001 From: Damian Suess Date: Sun, 28 Apr 2024 10:11:23 -0400 Subject: [PATCH] Imported Prism.Avalonia v9.0.401.11000-pre raw files (unconverted) --- PrismLibrary_Avalonia.slnf | 14 + .../Prism.Avalonia/Common/MvvmHelpers.cs | 80 ++ .../Prism.Avalonia/Common/ObservableObject.cs | 57 + src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs | 76 ++ .../Prism.Avalonia/Dialogs/DialogService.cs | 176 +++ .../Prism.Avalonia/Dialogs/DialogWindow.axaml | 13 + .../Dialogs/DialogWindow.axaml.cs | 29 + .../Dialogs/IDialogServiceCompatExtensions.cs | 63 + .../Prism.Avalonia/Dialogs/IDialogWindow.cs | 70 + .../Dialogs/IDialogWindowExtensions.cs | 18 + .../Dialogs/KnownDialogParameters.cs | 14 + .../Extensions/AvaloniaObjectExtensions.cs | 17 + .../Extensions/CollectionExtensions.cs | 33 + .../Interactivity/CommandBehaviorBase.cs | 127 ++ .../Interactivity/InvokeCommandAction.cs | 226 ++++ .../Ioc/ContainerProviderExtension.cs | 70 + .../Ioc/IContainerRegistryExtensions.cs | 101 ++ .../Modularity/AssemblyResolver.Desktop.cs | 136 ++ .../ConfigurationModuleCatalog.Desktop.cs | 70 + .../Modularity/ConfigurationStore.Desktop.cs | 19 + .../DirectoryModuleCatalog.net45.cs | 247 ++++ .../DirectoryModuleCatalog.netcore.cs | 213 +++ .../FileModuleTypeLoader.Desktop.cs | 182 +++ .../Modularity/IAssemblyResolver.Desktop.cs | 14 + .../Modularity/IConfigurationStore.Desktop.cs | 14 + .../Modularity/IModuleCatalogExtensions.cs | 187 +++ .../Modularity/IModuleGroupsCatalog.cs | 17 + .../Modularity/IModuleTypeLoader.cs | 36 + .../Modularity/ModuleAttribute.Desktop.cs | 25 + .../Modularity/ModuleCatalog.cs | 69 + .../ModuleConfigurationElement.Desktop.cs | 88 ++ ...eConfigurationElementCollection.Desktop.cs | 141 ++ .../ModuleDependencyCollection.Desktop.cs | 88 ++ ...eDependencyConfigurationElement.Desktop.cs | 37 + .../Modularity/ModuleInfo.Desktop.cs | 9 + .../Prism.Avalonia/Modularity/ModuleInfo.cs | 113 ++ .../Modularity/ModuleInfoGroup.cs | 349 +++++ .../Modularity/ModuleInfoGroupExtensions.cs | 56 + .../Modularity/ModuleInitializer.cs | 118 ++ .../Modularity/ModuleManager.Desktop.cs | 36 + .../Modularity/ModuleManager.cs | 316 +++++ ...duleTypeLoaderNotFoundException.Desktop.cs | 19 + .../ModuleTypeLoaderNotFoundException.cs | 53 + .../ModulesConfigurationSection.Desktop.cs | 22 + .../Modularity/XamlModuleCatalog.cs | 121 ++ .../Prism.Avalonia/Mvvm/ViewModelLocator.cs | 69 + .../Navigation/Regions/AllActiveRegion.cs | 27 + .../Behaviors/AutoPopulateRegionBehavior.cs | 100 ++ ...ndRegionContextToAvaloniaObjectBehavior.cs | 102 ++ .../ClearChildViewsRegionBehavior.cs | 89 ++ .../DelayedRegionCreationBehavior.cs | 227 ++++ .../Behaviors/DestructibleRegionBehavior.cs | 41 + .../Behaviors/IHostAwareRegionBehavior.cs | 18 + .../Behaviors/RegionActiveAwareBehavior.cs | 131 ++ .../RegionManagerRegistrationBehavior.cs | 158 +++ .../Behaviors/RegionMemberLifetimeBehavior.cs | 109 ++ .../SelectorItemsSourceSyncBehavior.cs | 191 +++ .../SyncRegionContextWithHostBehavior.cs | 110 ++ .../Regions/ContentControlRegionAdapter.cs | 64 + .../Regions/DefaultRegionManagerAccessor.cs | 46 + .../Navigation/Regions/INavigationAware.cs | 8 + .../Regions/IRegionManagerAccessor.cs | 33 + .../Navigation/Regions/ItemMetadata.cs | 65 + .../Regions/ItemsControlRegionAdapter.cs | 71 + .../Navigation/Regions/Region.cs | 448 +++++++ .../Navigation/Regions/RegionAdapterBase.cs | 154 +++ .../Regions/RegionAdapterMappings.cs | 101 ++ .../Navigation/Regions/RegionContext.cs | 50 + .../Navigation/Regions/RegionManager.cs | 550 ++++++++ .../Regions/RegionNavigationContentLoader.cs | 181 +++ .../Regions/RegionNavigationService.cs | 264 ++++ .../Navigation/Regions/RegionViewRegistry.cs | 113 ++ .../Regions/SelectorRegionAdapter.cs | 70 + .../Navigation/Regions/SingleActiveRegion.cs | 28 + .../Navigation/Regions/ViewsCollection.cs | 298 ++++ .../Prism.Avalonia/Prism.Avalonia.csproj | 54 + .../Prism.Avalonia/PrismApplicationBase.cs | 187 +++ .../Prism.Avalonia/PrismBootstrapperBase.cs | 183 +++ .../PrismInitializationExtensions.cs | 66 + .../Prism.Avalonia/Properties/AssemblyInfo.cs | 13 + .../Properties/Resources.Designer.cs | 523 ++++++++ .../Prism.Avalonia/Properties/Resources.resx | 280 ++++ .../Properties/Settings.Designer.cs | 26 + .../Properties/Settings.settings | 7 + .../GlobalSuppressions.cs | 11 + .../Prism.DryIoc.Avalonia.csproj | 38 + .../Prism.DryIoc.Avalonia/PrismApplication.cs | 38 + .../PrismBootstrapper.cs | 31 + .../Properties/AssemblyInfo.cs | 12 + .../Properties/Resources.Designer.cs | 270 ++++ .../Properties/Resources.resx | 189 +++ .../build/Package.targets | 7 + src/Avalonia/ReadMe.md | 16 + .../CollectionChangedTracker.cs | 27 + .../CollectionExtensionsFixture.cs | 23 + .../CompilerHelper.Desktop.cs | 191 +++ .../Prism.Avalonia.Tests/ExceptionAssert.cs | 28 + .../CommandBehaviorBaseFixture.cs | 169 +++ .../InvokeCommandActionFixture.cs | 383 ++++++ .../ListDictionaryFixture.cs | 260 ++++ .../Mocks/MockAsyncModuleTypeLoader.cs | 49 + .../Mocks/MockClickableObject.cs | 12 + .../Prism.Avalonia.Tests/Mocks/MockCommand.cs | 34 + .../Mocks/MockConfigurationStore.Desktop.cs | 19 + .../Mocks/MockContainerAdapter.cs | 142 ++ .../Mocks/MockDelegateReference.cs | 18 + .../Mocks/MockDependencyObject.cs | 10 + .../Mocks/MockFrameworkContentElement.cs | 27 + .../Mocks/MockFrameworkElement.cs | 28 + .../Mocks/MockHostAwareRegionBehavior.cs | 17 + .../MockInteractionRequestAwareElement.cs | 15 + .../Mocks/MockModuleTypeLoader.cs | 41 + .../Mocks/MockPresentationRegion.cs | 142 ++ .../Prism.Avalonia.Tests/Mocks/MockRegion.cs | 114 ++ .../Mocks/MockRegionAdapter.cs | 24 + .../Mocks/MockRegionBehavior.cs | 19 + .../Mocks/MockRegionBehaviorCollection.cs | 12 + .../Mocks/MockRegionManager.cs | 136 ++ .../Mocks/MockRegionManagerAccessor.cs | 40 + .../Mocks/MockSortableViews.cs | 17 + .../Mocks/MockViewsCollection.cs | 38 + .../Mocks/Modules/MockAbstractModule.cs | 23 + .../Mocks/Modules/MockAttributedModule.cs | 19 + .../Mocks/Modules/MockDependantModule.cs | 20 + .../Mocks/Modules/MockDependencyModule.cs | 19 + .../MockExposingTypeFromGacAssemblyModule.cs | 36 + .../Mocks/Modules/MockModuleA.cs | 22 + .../Modules/MockModuleReferencedAssembly.cs | 6 + .../Modules/MockModuleReferencingAssembly.cs | 18 + .../MockModuleReferencingOtherModule.cs | 21 + .../Modules/MockModuleThrowingException.cs | 18 + .../Mocks/ViewModels/MockOptOutViewModel.cs | 8 + .../Mocks/ViewModels/MockViewModel.cs | 27 + .../Prism.Avalonia.Tests/Mocks/Views/Mock.cs | 8 + .../Mocks/Views/MockOptOut.cs | 13 + .../Mocks/Views/MockView.cs | 8 + .../AssemblyResolverFixture.Desktop.cs | 127 ++ ...nfigurationModuleCatalogFixture.Desktop.cs | 160 +++ .../ConfigurationStoreFixture.Desktop.cs | 26 + .../DirectoryModuleCatalogFixture.Desktop.cs | 563 ++++++++ .../FileModuleTypeLoaderFixture.Desktop.cs | 139 ++ .../ModuleAttributeFixture.Desktop.cs | 35 + .../Modularity/ModuleCatalogFixture.cs | 478 +++++++ .../InvalidDependencyModuleCatalog.xaml | 18 + .../SimpleModuleCatalog.xaml | 28 + .../ModuleDependencySolverFixture.cs | 148 ++ .../ModuleInfoGroupExtensionsFixture.cs | 82 ++ .../Modularity/ModuleInfoGroupFixture.cs | 21 + .../Modularity/ModuleInitializerFixture.cs | 233 ++++ .../Modularity/ModuleManagerFixture.cs | 507 +++++++ .../Modularity/NotAValidDotNetDll.txt.dll | 1 + .../Mvvm/ViewModelLocatorFixture.cs | 94 ++ .../Prism.Avalonia.Tests.csproj | 40 + .../PrismApplicationBaseFixture.cs | 350 +++++ .../PrismBootstrapperBaseFixture.cs | 344 +++++ .../Regions/AllActiveRegionFixture.cs | 32 + .../AutoPopulateRegionBehaviorFixture.cs | 124 ++ ...nContextToAvaloniaObjectBehaviorFixture.cs | 97 ++ .../ClearChildViewsRegionBehaviorFixture.cs | 79 ++ .../DelayedRegionCreationBehaviorFixture.cs | 198 +++ .../RegionActiveAwareBehaviorFixture.cs | 330 +++++ ...egionManagerRegistrationBehaviorFixture.cs | 358 +++++ .../RegionMemberLifetimeBehaviorFixture.cs | 259 ++++ ...torItemsSourceSyncRegionBehaviorFixture.cs | 226 ++++ ...yncRegionContextWithHostBehaviorFixture.cs | 139 ++ .../ContentControlRegionAdapterFixture.cs | 161 +++ .../ItemsControlRegionAdapterFixture.cs | 126 ++ .../LocatorNavigationTargetHandlerFixture.cs | 343 +++++ .../NavigationAsyncExtensionsFixture.cs | 104 ++ .../Regions/NavigationContextFixture.cs | 45 + .../Regions/RegionAdapterBaseFixture.cs | 108 ++ .../Regions/RegionAdapterMappingsFixture.cs | 146 ++ .../RegionBehaviorCollectionFixture.cs | 40 + .../Regions/RegionBehaviorFactoryFixture.cs | 72 + .../Regions/RegionBehaviorFixture.cs | 56 + .../Regions/RegionFixture.cs | 647 +++++++++ .../Regions/RegionManagerFixture.cs | 499 +++++++ .../RegionManagerRequestNavigateFixture.cs | 126 ++ .../Regions/RegionNavigationJournalFixture.cs | 487 +++++++ .../RegionNavigationServiceFixture.new.cs | 1193 +++++++++++++++++ .../Regions/RegionViewRegistryFixture.cs | 198 +++ .../Regions/SelectorRegionAdapterFixture.cs | 113 ++ .../Regions/SingleActiveRegionFixture.cs | 28 + .../Regions/ViewsCollectionFixture.cs | 344 +++++ .../DryIocBootstrapperFixture.cs | 273 ++++ .../DryIocBootstrapperNullContainerFixture.cs | 38 + .../DryIocBootstrapperNullLoggerFixture.cs | 38 + ...IocBootstrapperNullModuleCatalogFixture.cs | 38 + ...IocBootstrapperNullModuleManagerFixture.cs | 55 + ...ootstrapperRegisterForNavigationFixture.cs | 37 + .../DryIocBootstrapperRunMethodFixture.cs | 473 +++++++ .../Prism.DryIoc.Avalonia.Tests.csproj | 21 + 192 files changed, 23192 insertions(+) create mode 100644 PrismLibrary_Avalonia.slnf create mode 100644 src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs create mode 100644 src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs create mode 100644 src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Extensions/CollectionExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs create mode 100644 src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs create mode 100644 src/Avalonia/Prism.Avalonia/Ioc/ContainerProviderExtension.cs create mode 100644 src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs create mode 100644 src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs create mode 100644 src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemsControlRegionAdapter.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs create mode 100644 src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs create mode 100644 src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj create mode 100644 src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs create mode 100644 src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs create mode 100644 src/Avalonia/Prism.Avalonia/PrismInitializationExtensions.cs create mode 100644 src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs create mode 100644 src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs create mode 100644 src/Avalonia/Prism.Avalonia/Properties/Resources.resx create mode 100644 src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs create mode 100644 src/Avalonia/Prism.Avalonia/Properties/Settings.settings create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/GlobalSuppressions.cs create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx create mode 100644 src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets create mode 100644 src/Avalonia/ReadMe.md create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockAsyncModuleTypeLoader.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockClickableObject.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockCommand.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockConfigurationStore.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockContainerAdapter.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDelegateReference.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDependencyObject.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkContentElement.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockFrameworkElement.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockHostAwareRegionBehavior.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockInteractionRequestAwareElement.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockModuleTypeLoader.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockPresentationRegion.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegion.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionAdapter.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehavior.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehaviorCollection.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManager.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManagerAccessor.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockSortableViews.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockViewsCollection.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAbstractModule.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAttributedModule.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependantModule.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependencyModule.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockExposingTypeFromGacAssemblyModule.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleA.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencedAssembly.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingAssembly.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingOtherModule.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleThrowingException.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockOptOutViewModel.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/ViewModels/MockViewModel.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/Mock.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockOptOut.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mocks/Views/MockView.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/AssemblyResolverFixture.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationModuleCatalogFixture.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationStoreFixture.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/DirectoryModuleCatalogFixture.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/FileModuleTypeLoaderFixture.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleAttributeFixture.Desktop.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/InvalidDependencyModuleCatalog.xaml create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogXaml/SimpleModuleCatalog.xaml create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleDependencySolverFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupExtensionsFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInitializerFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleManagerFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Modularity/NotAValidDotNetDll.txt.dll create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Mvvm/ViewModelLocatorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Prism.Avalonia.Tests.csproj create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/PrismApplicationBaseFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/PrismBootstrapperBaseFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/AllActiveRegionFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/ClearChildViewsRegionBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/DelayedRegionCreationBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionActiveAwareBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionManagerRegistrationBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionMemberLifetimeBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SelectorItemsSourceSyncRegionBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SyncRegionContextWithHostBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/ContentControlRegionAdapterFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/ItemsControlRegionAdapterFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/LocatorNavigationTargetHandlerFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationAsyncExtensionsFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationContextFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterBaseFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterMappingsFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorCollectionFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFactoryFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerRequestNavigateFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationJournalFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationServiceFixture.new.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/SelectorRegionAdapterFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/SingleActiveRegionFixture.cs create mode 100644 tests/Avalonia/Prism.Avalonia.Tests/Regions/ViewsCollectionFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullContainerFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullLoggerFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleCatalogFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleManagerFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRegisterForNavigationFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRunMethodFixture.cs create mode 100644 tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Prism.DryIoc.Avalonia.Tests.csproj diff --git a/PrismLibrary_Avalonia.slnf b/PrismLibrary_Avalonia.slnf new file mode 100644 index 0000000000..1b49b7e34a --- /dev/null +++ b/PrismLibrary_Avalonia.slnf @@ -0,0 +1,14 @@ +{ + "solution": { + "path": "Prism.Avalonia.sln", + "projects": [ + "src\\Containers\\Prism.DryIoc.Shared\\Prism.DryIoc.Shared.shproj", + "src\\Avalonia\\Prism.Avalonia\\Prism.Avalonia.csproj", + "src\\Avalonia\\Prism.DryIoc.Avalonia\\Prism.DryIoc.Avalonia.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" + ] + } +} \ No newline at end of file diff --git a/src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs b/src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs new file mode 100644 index 0000000000..a28ec34cd2 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs @@ -0,0 +1,80 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Prism.Mvvm; + +namespace Prism.Common +{ + /// + /// Helper class for MVVM. + /// + public static class MvvmHelpers + { + /// + /// Sets the AutoWireViewModel property to true for the . + /// + /// + /// The AutoWireViewModel property will only be set to true if the view + /// is a , the DataContext of the view is null, and + /// the AutoWireViewModel property of the view is null. + /// + /// The View or ViewModel. + [EditorBrowsable(EditorBrowsableState.Never)] + internal static void AutowireViewModel(object viewOrViewModel) + { + if (viewOrViewModel is Control view && + view.DataContext is null && + ViewModelLocator.GetAutoWireViewModel(view) is null) + { + ViewModelLocator.SetAutoWireViewModel(view, true); + } + } + + ////#endif + + /// + /// Perform an on a view and ViewModel. + /// + /// + /// The action will be performed on the view and its ViewModel if they implement . + /// + /// The parameter type. + /// The view to perform the on. + /// The to perform. + public static void ViewAndViewModelAction(object view, Action action) where T : class + { + if (view is T viewAsT) + action(viewAsT); + + if (view is Control element && element.DataContext is T viewModelAsT) + { + action(viewModelAsT); + } + } + + /// + /// Get an implementer from a view or ViewModel. + /// + /// + /// If the view implements it will be returned. + /// Otherwise if the view's implements it will be returned instead. + /// + /// The implementer type to get. + /// The view to get from. + /// view or ViewModel as . + public static T GetImplementerFromViewOrViewModel(object view) where T : class + { + if (view is T viewAsT) + { + return viewAsT; + } + + if (view is Control element && element.DataContext is T vmAsT) + { + return vmAsT; + } + + return null; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs b/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs new file mode 100644 index 0000000000..cde4a6d88a --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +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 WPF dependency properties")] + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(name: nameof(Value)); + + //StyledProperty.Register("Value", typeof(T), typeof(ObservableObject), new PropertyMetadata(ValueChangedCallback)); + + /// + /// 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 { return (T)this.GetValue(ValueProperty); } + set { this.SetValue(ValueProperty, value); } + } + + private static void ValueChangedCallback(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + ObservableObject thisInstance = ((ObservableObject)d); + PropertyChangedEventHandler eventHandler = thisInstance.PropertyChanged; + if (eventHandler != null) + { + eventHandler(thisInstance, new PropertyChangedEventArgs(nameof(Value))); + } + } + + static ObservableObject() + { + ValueProperty.Changed.Subscribe(args => ValueChangedCallback(args?.Sender, args)); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs b/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs new file mode 100644 index 0000000000..bfdc794627 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Styling; + +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)); + + 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..54faeaad47 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs @@ -0,0 +1,176 @@ +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; + } + + 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 a FrameworkElement"); + + 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..b97c3bc63c --- /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..5c322549e4 --- /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..eb9b8384f5 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs @@ -0,0 +1,63 @@ +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. + 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..c367f1f3cb --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs @@ -0,0 +1,70 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Styling; + +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 ShowDialog(Window owner); + + /// + /// The data context of the window. + /// + /// + /// The data context must implement . + /// + object DataContext { get; set; } + + /// Called when the window is 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 + /// + //// WPF: event RoutedEventHandler Loaded; + event EventHandler Opened; + + /// + /// Called when the window is closed. + /// + event EventHandler Closed; + + /// + /// Called when the window is closing. + /// + // WPF: event CancelEventHandler Closing; + // Ava: ... + 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..d60158aa03 --- /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..b5314049ea --- /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..51207195d5 --- /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/CollectionExtensions.cs b/src/Avalonia/Prism.Avalonia/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000000..34a23c00bd --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Extensions/CollectionExtensions.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace System.Collections.ObjectModel +{ + /// + /// Class that provides extension methods to Collection + /// + public static class CollectionExtensions + { + /// + /// Add a range of items to a collection. + /// + /// Type of objects within the collection. + /// The collection to add items to. + /// The items to add to the collection. + /// The collection. + /// An is thrown if or is . + public static Collection AddRange(this Collection collection, IEnumerable items) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + if (items == null) + throw new ArgumentNullException(nameof(items)); + + foreach (var each in items) + { + collection.Add(each); + } + + return collection; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs b/src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs new file mode 100644 index 0000000000..b2f81082a1 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs @@ -0,0 +1,127 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls; + +namespace Prism.Interactivity +{ + /// + /// Base behavior to handle connecting a to a Command. + /// + /// The target object must derive from Control. + /// + /// CommandBehaviorBase can be used to provide new behaviors for commands. + /// + public class CommandBehaviorBase where T : Control + { + private ICommand _command; + private object _commandParameter; + private readonly WeakReference _targetObject; + private readonly EventHandler _commandCanExecuteChangedHandler; + + /// + /// Constructor specifying the target object. + /// + /// The target object the behavior is attached to. + public CommandBehaviorBase(T targetObject) + { + _targetObject = new WeakReference(targetObject); + + _commandCanExecuteChangedHandler = CommandCanExecuteChanged; + } + + bool _autoEnabled = true; + /// + /// If true the target object's IsEnabled property will update based on the commands ability to execute. + /// If false the target object's IsEnabled property will not update. + /// + public bool AutoEnable + { + get { return _autoEnabled; } + set + { + _autoEnabled = value; + UpdateEnabledState(); + } + } + + /// + /// Corresponding command to be execute and monitored for . + /// + public ICommand Command + { + get { return _command; } + set + { + if (_command != null) + { + _command.CanExecuteChanged -= _commandCanExecuteChangedHandler; + } + + _command = value; + if (_command != null) + { + _command.CanExecuteChanged += _commandCanExecuteChangedHandler; + UpdateEnabledState(); + } + } + } + + /// + /// The parameter to supply the command during execution. + /// + public object CommandParameter + { + get { return _commandParameter; } + set + { + if (_commandParameter != value) + { + _commandParameter = value; + UpdateEnabledState(); + } + } + } + + /// + /// Object to which this behavior is attached. + /// + protected T TargetObject + { + get + { + return _targetObject.Target as T; + } + } + + /// + /// Updates the target object's IsEnabled property based on the commands ability to execute. + /// + protected virtual void UpdateEnabledState() + { + if (TargetObject == null) + { + Command = null; + CommandParameter = null; + } + else if (Command != null) + { + if (AutoEnable) + TargetObject.IsEnabled = Command.CanExecute(CommandParameter); + } + } + + private void CommandCanExecuteChanged(object sender, EventArgs e) + { + UpdateEnabledState(); + } + + /// + /// Executes the command, if it's set, providing the . + /// + protected virtual void ExecuteCommand(object parameter) + { + if (Command != null) + Command.Execute(CommandParameter ?? parameter); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs b/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs new file mode 100644 index 0000000000..d86a095fd4 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs @@ -0,0 +1,226 @@ +// 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/Ioc/ContainerProviderExtension.cs b/src/Avalonia/Prism.Avalonia/Ioc/ContainerProviderExtension.cs new file mode 100644 index 0000000000..f8c25e3a3e --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Ioc/ContainerProviderExtension.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Markup.Xaml; + +namespace Prism.Ioc +{ + /// + /// Provides Types and Services registered with the Container + /// + /// + /// Usage as markup extension: + /// + /// ]]> + /// + /// + /// Usage as XML element: + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + public class ContainerProviderExtension : MarkupExtension + { + /// + /// Initializes a new instance of the class. + /// + public ContainerProviderExtension() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type to Resolve + public ContainerProviderExtension(Type type) + { + Type = type; + } + + /// + /// The type to Resolve + /// + public Type Type { get; set; } + + /// + /// The Name used to register the type with the Container + /// + public string Name { get; set; } + + /// + /// Provide resolved object from + /// + /// + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + return string.IsNullOrEmpty(Name) + ? ContainerLocator.Container?.Resolve(Type) + : ContainerLocator.Container?.Resolve(Type, Name); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs b/src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs new file mode 100644 index 0000000000..0ab09e2a1f --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs @@ -0,0 +1,101 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Prism.Mvvm; + +namespace Prism.Ioc +{ + /// + /// extensions. + /// + public static class IContainerRegistryExtensions + { + /// + /// Registers an object to be used as a dialog in the IDialogService. + /// + /// The Type of object to register as the dialog + /// + /// The unique name to register with the dialog. + public static void RegisterDialog<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TView>(this IContainerRegistry containerRegistry, string name = null) + { + containerRegistry.RegisterForNavigation(name); + } + + /// + /// Registers an object to be used as a dialog in the IDialogService. + /// + /// The Type of object to register as the dialog + /// The ViewModel to use as the DataContext for the dialog + /// + /// The unique name to register with the dialog. + public static void RegisterDialog<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TView, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TViewModel>(this IContainerRegistry containerRegistry, string name = null) where TViewModel : Dialogs.IDialogAware + { + containerRegistry.RegisterForNavigation(name); + } + + /// + /// Registers an object that implements IDialogWindow to be used to host all dialogs in the IDialogService. + /// + /// The Type of the Window class that will be used to host dialogs in the IDialogService + /// + public static void RegisterDialogWindow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWindow>(this IContainerRegistry containerRegistry) where TWindow : Dialogs.IDialogWindow + { + containerRegistry.Register(typeof(Dialogs.IDialogWindow), typeof(TWindow)); + } + + /// + /// Registers an object that implements IDialogWindow to be used to host all dialogs in the IDialogService. + /// + /// The Type of the Window class that will be used to host dialogs in the IDialogService + /// + /// The name of the dialog window + public static void RegisterDialogWindow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWindow>(this IContainerRegistry containerRegistry, string name) where TWindow : Dialogs.IDialogWindow + { + containerRegistry.Register(typeof(Dialogs.IDialogWindow), typeof(TWindow), name); + } + + /// + /// Registers an object for navigation + /// + /// + /// The type of object to register + /// The unique name to register with the object. + public static void RegisterForNavigation(this IContainerRegistry containerRegistry, Type type, string name) + { + containerRegistry.Register(typeof(object), type, name); + } + + /// + /// Registers an object for navigation. + /// + /// The Type of the object to register as the view + /// + /// The unique name to register with the object. + public static void RegisterForNavigation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this IContainerRegistry containerRegistry, string name = null) + { + Type type = typeof(T); + string viewName = string.IsNullOrWhiteSpace(name) ? type.Name : name; + containerRegistry.RegisterForNavigation(type, viewName); + } + + /// + /// Registers an object for navigation with the ViewModel type to be used as the DataContext. + /// + /// The Type of object to register as the view + /// The ViewModel to use as the DataContext for the view + /// + /// The unique name to register with the view + public static void RegisterForNavigation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TView, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TViewModel>(this IContainerRegistry containerRegistry, string name = null) + { + containerRegistry.RegisterForNavigationWithViewModel(typeof(TView), name); + } + + private static void RegisterForNavigationWithViewModel<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TViewModel>(this IContainerRegistry containerRegistry, Type viewType, string name) + { + if (string.IsNullOrWhiteSpace(name)) + name = viewType.Name; + + ViewModelLocationProvider.Register(viewType.ToString(), typeof(TViewModel)); + containerRegistry.RegisterForNavigation(viewType, name); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs new file mode 100644 index 0000000000..5681e11391 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Prism.Properties; + +namespace Prism.Modularity +{ + /// + /// Handles AppDomain's AssemblyResolve event to be able to load assemblies dynamically in + /// the LoadFrom context, but be able to reference the type from assemblies loaded in the Load context. + /// + public class AssemblyResolver : IAssemblyResolver, IDisposable + { + private readonly List registeredAssemblies = new List(); + + private bool handlesAssemblyResolve; + + /// + /// Registers the specified assembly and resolves the types in it when the AppDomain requests for it. + /// + /// The path to the assembly to load in the LoadFrom context. + /// This method does not load the assembly immediately, but lazily until someone requests a + /// declared in the assembly. + public void LoadAssemblyFrom(string assemblyFilePath) + { + if (!this.handlesAssemblyResolve) + { + AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve; + this.handlesAssemblyResolve = true; + } + + Uri assemblyUri = GetFileUri(assemblyFilePath); + + if (assemblyUri == null) + { + throw new ArgumentException(Resources.InvalidArgumentAssemblyUri, nameof(assemblyFilePath)); + } + + if (!File.Exists(assemblyUri.LocalPath)) + { + throw new FileNotFoundException(null, assemblyUri.LocalPath); + } + + AssemblyName assemblyName = AssemblyName.GetAssemblyName(assemblyUri.LocalPath); + AssemblyInfo assemblyInfo = this.registeredAssemblies.FirstOrDefault(a => assemblyName == a.AssemblyName); + + if (assemblyInfo != null) + { + return; + } + + assemblyInfo = new AssemblyInfo() { AssemblyName = assemblyName, AssemblyUri = assemblyUri }; + this.registeredAssemblies.Add(assemblyInfo); + } + + private static Uri GetFileUri(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + return null; + } + + Uri uri; + if (!Uri.TryCreate(filePath, UriKind.Absolute, out uri)) + { + return null; + } + + if (!uri.IsFile) + { + return null; + } + + return uri; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")] + private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + AssemblyName assemblyName = new AssemblyName(args.Name); + + AssemblyInfo assemblyInfo = this.registeredAssemblies.FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.AssemblyName)); + + if (assemblyInfo != null) + { + if (assemblyInfo.Assembly == null) + { + assemblyInfo.Assembly = Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath); + } + + return assemblyInfo.Assembly; + } + + return null; + } + + private class AssemblyInfo + { + public AssemblyName AssemblyName { get; set; } + + public Uri AssemblyUri { get; set; } + + public Assembly Assembly { get; set; } + } + + #region Implementation of IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Calls . + /// 2 + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the associated . + /// + /// When , it is being called from the Dispose method. + protected virtual void Dispose(bool disposing) + { + if (this.handlesAssemblyResolve) + { + AppDomain.CurrentDomain.AssemblyResolve -= this.CurrentDomain_AssemblyResolve; + this.handlesAssemblyResolve = false; + } + } + + #endregion + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs new file mode 100644 index 0000000000..552f64ded5 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Prism.Properties; + +namespace Prism.Modularity +{ + + /// + /// A catalog built from a configuration file. + /// + public class ConfigurationModuleCatalog : ModuleCatalog + { + /// + /// Builds an instance of ConfigurationModuleCatalog with a as the default store. + /// + public ConfigurationModuleCatalog() + { + Store = new ConfigurationStore(); + } + + /// + /// Gets or sets the store where the configuration is kept. + /// + public IConfigurationStore Store { get; set; } + + /// + /// Loads the catalog from the configuration. + /// + protected override void InnerLoad() + { + if (Store == null) + { + throw new InvalidOperationException(Resources.ConfigurationStoreCannotBeNull); + } + + EnsureModulesDiscovered(); + } + + private void EnsureModulesDiscovered() + { + ModulesConfigurationSection section = Store.RetrieveModuleConfigurationSection(); + + if (section != null) + { + foreach (ModuleConfigurationElement element in section.Modules) + { + IList dependencies = new List(); + + if (element.Dependencies.Count > 0) + { + foreach (ModuleDependencyConfigurationElement dependency in element.Dependencies) + { + dependencies.Add(dependency.ModuleName); + } + } + + ModuleInfo moduleInfo = new ModuleInfo(element.ModuleName, element.ModuleType) + { + Ref = GetFileAbsoluteUri(element.AssemblyFile), + InitializationMode = element.StartupLoaded ? InitializationMode.WhenAvailable : InitializationMode.OnDemand + }; + moduleInfo.DependsOn.AddRange(dependencies.ToArray()); + AddModule(moduleInfo); + } + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs new file mode 100644 index 0000000000..53081bacb1 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs @@ -0,0 +1,19 @@ +using System.Configuration; + +namespace Prism.Modularity +{ + /// + /// Defines a store for the module metadata. + /// + public class ConfigurationStore : IConfigurationStore + { + /// + /// Gets the module configuration data. + /// + /// A instance. + public ModulesConfigurationSection RetrieveModuleConfigurationSection() + { + return ConfigurationManager.GetSection("modules") as ModulesConfigurationSection; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs new file mode 100644 index 0000000000..d4c3465bd5 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Policy; +using Prism.Properties; + +namespace Prism.Modularity +{ + /// + /// Represets a catalog created from a directory on disk. + /// + /// + /// The directory catalog will scan the contents of a directory, locating classes that implement + /// and add them to the catalog based on contents in their associated . + /// Assemblies are loaded into a new application domain with ReflectionOnlyLoad. The application domain is destroyed + /// once the assemblies have been discovered. + /// + /// The diretory catalog does not continue to monitor the directory after it has created the initialze catalog. + /// + public class DirectoryModuleCatalog : ModuleCatalog + { + /// + /// Directory containing modules to search for. + /// + public string ModulePath { get; set; } + + /// + /// Drives the main logic of building the child domain and searching for the assemblies. + /// + protected override void InnerLoad() + { + if (string.IsNullOrEmpty(this.ModulePath)) + throw new InvalidOperationException(Resources.ModulePathCannotBeNullOrEmpty); + + if (!Directory.Exists(this.ModulePath)) + throw new InvalidOperationException( + string.Format(CultureInfo.CurrentCulture, Resources.DirectoryNotFound, this.ModulePath)); + + AppDomain childDomain = this.BuildChildDomain(AppDomain.CurrentDomain); + + try + { + 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" + // TODO: Do this in a less hacky way... probably never gonna happen + && !assembly.GetName().Name.StartsWith("xunit") + && !string.IsNullOrEmpty(assembly.Location) + select assembly.Location + ); + + loadedAssemblies.AddRange(assemblies); + + Type loaderType = typeof(InnerModuleInfoLoader); + + if (loaderType.Assembly != null) + { + var loader = + (InnerModuleInfoLoader) + childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap(); + loader.LoadAssemblies(loadedAssemblies); + this.Items.AddRange(loader.GetModuleInfos(this.ModulePath)); + } + } + finally + { + AppDomain.Unload(childDomain); + } + } + + + /// + /// Creates a new child domain and copies the evidence from a parent domain. + /// + /// The parent domain. + /// The new child domain. + /// + /// Grabs the evidence and uses it to construct the new + /// because in a ClickOnce execution environment, creating an + /// will by default pick up the partial trust environment of + /// the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a + /// create domain and applies the evidence from the ClickOnce manifests to + /// create the domain that the application is actually executing in. This will + /// need to be Full Trust for Prism applications. + /// + /// An is thrown if is null. + protected virtual AppDomain BuildChildDomain(AppDomain parentDomain) + { + if (parentDomain == null) + throw new ArgumentNullException(nameof(parentDomain)); + + Evidence evidence = new Evidence(parentDomain.Evidence); + AppDomainSetup setup = parentDomain.SetupInformation; + return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup); + } + + private class InnerModuleInfoLoader : MarshalByRefObject + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + internal ModuleInfo[] GetModuleInfos(string path) + { + DirectoryInfo directory = new DirectoryInfo(path); + + ResolveEventHandler resolveEventHandler = + delegate (object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, directory); }; + + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler; + + Assembly moduleReflectionOnlyAssembly = + AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First( + asm => asm.FullName == typeof(IModule).Assembly.FullName); + Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName); + + IEnumerable modules = GetNotAlreadyLoadedModuleInfos(directory, IModuleType); + + var array = modules.ToArray(); + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler; + return array; + } + + private static IEnumerable GetNotAlreadyLoadedModuleInfos(DirectoryInfo directory, Type IModuleType) + { + List validAssemblies = new List(); + Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies(); + + var fileInfos = directory.GetFiles("*.dll") + .Where(file => alreadyLoadedAssemblies + .FirstOrDefault( + assembly => + String.Compare(Path.GetFileName(assembly.Location), file.Name, + StringComparison.OrdinalIgnoreCase) == 0) == null); + + foreach (FileInfo fileInfo in fileInfos) + { + try + { + Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName); + validAssemblies.Add(fileInfo); + } + catch (BadImageFormatException) + { + // skip non-.NET Dlls + } + } + + return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName) + .GetExportedTypes() + .Where(IModuleType.IsAssignableFrom) + .Where(t => t != IModuleType) + .Where(t => !t.IsAbstract) + .Select(type => CreateModuleInfo(type))); + } + + private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory) + { + Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault( + asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase)); + if (loadedAssembly != null) + { + 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); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + internal void LoadAssemblies(IEnumerable assemblies) + { + foreach (string assemblyPath in assemblies) + { + try + { + Assembly.ReflectionOnlyLoadFrom(assemblyPath); + } + catch (FileNotFoundException) + { + // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain + } + } + } + + private static ModuleInfo CreateModuleInfo(Type type) + { + string moduleName = type.Name; + List dependsOn = new List(); + bool onDemand = false; + var moduleAttribute = + CustomAttributeData.GetCustomAttributes(type).FirstOrDefault( + cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName); + + if (moduleAttribute != null) + { + foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments) + { + string argumentName = argument.MemberInfo.Name; + switch (argumentName) + { + case "ModuleName": + moduleName = (string)argument.TypedValue.Value; + break; + + case "OnDemand": + onDemand = (bool)argument.TypedValue.Value; + break; + + case "StartupLoaded": + onDemand = !((bool)argument.TypedValue.Value); + break; + } + } + } + + var moduleDependencyAttributes = + CustomAttributeData.GetCustomAttributes(type).Where( + cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName); + + foreach (CustomAttributeData cad in moduleDependencyAttributes) + { + dependsOn.Add((string)cad.ConstructorArguments[0].Value); + } + + ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName) + { + InitializationMode = + onDemand + ? InitializationMode.OnDemand + : InitializationMode.WhenAvailable, + Ref = type.Assembly.EscapedCodeBase, + }; + moduleInfo.DependsOn.AddRange(dependsOn); + return moduleInfo; + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs new file mode 100644 index 0000000000..fd6693f278 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Prism.Properties; + +namespace Prism.Modularity +{ + /// + /// Represents a catalog created from a directory on disk. + /// + /// + /// The directory catalog will scan the contents of a directory, locating classes that implement + /// and add them to the catalog based on contents in their associated . + /// Assemblies are loaded into a new application domain with ReflectionOnlyLoad. The application domain is destroyed + /// once the assemblies have been discovered. + /// + /// The directory catalog does not continue to monitor the directory after it has created the initialize catalog. + /// + public class DirectoryModuleCatalog : ModuleCatalog + { + /// + /// Directory containing modules to search for. + /// + public string ModulePath { get; set; } + + /// + /// Drives the main logic of building the child domain and searching for the assemblies. + /// + protected override void InnerLoad() + { + if (string.IsNullOrEmpty(this.ModulePath)) + throw new InvalidOperationException(Resources.ModulePathCannotBeNullOrEmpty); + + if (!Directory.Exists(this.ModulePath)) + throw new InvalidOperationException( + string.Format(CultureInfo.CurrentCulture, Resources.DirectoryNotFound, this.ModulePath)); + + AppDomain childDomain = AppDomain.CurrentDomain; + + try + { + 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 + ); + + loadedAssemblies.AddRange(assemblies); + + Type loaderType = typeof(InnerModuleInfoLoader); + + if (loaderType.Assembly != null) + { + var loader = + (InnerModuleInfoLoader) + childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap(); + + this.Items.AddRange(loader.GetModuleInfos(this.ModulePath)); + } + } + catch (Exception ex) + { + throw new Exception("There was an error loading assemblies.", ex); + } + } + + private class InnerModuleInfoLoader : MarshalByRefObject + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + internal ModuleInfo[] GetModuleInfos(string path) + { + DirectoryInfo directory = new DirectoryInfo(path); + + ResolveEventHandler resolveEventHandler = + delegate (object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, directory); }; + + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler; + + Assembly moduleReflectionOnlyAssembly = AppDomain.CurrentDomain.GetAssemblies().First(asm => asm.FullName == typeof(IModule).Assembly.FullName); + Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName); + + IEnumerable modules = GetNotAlreadyLoadedModuleInfos(directory, IModuleType); + + var array = modules.ToArray(); + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler; + return array; + } + + private static IEnumerable GetNotAlreadyLoadedModuleInfos(DirectoryInfo directory, Type IModuleType) + { + List validAssemblies = new List(); + Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(p => !p.IsDynamic).ToArray(); + + var fileInfos = directory.GetFiles("*.dll") + .Where(file => alreadyLoadedAssemblies.FirstOrDefault( + assembly => String.Compare(Path.GetFileName(assembly.Location), + file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null).ToList(); + + foreach (FileInfo fileInfo in fileInfos) + { + try + { + validAssemblies.Add(Assembly.LoadFrom(fileInfo.FullName)); + } + catch (BadImageFormatException) + { + // skip non-.NET Dlls + } + } + + return validAssemblies.SelectMany(assembly => assembly + .GetExportedTypes() + .Where(IModuleType.IsAssignableFrom) + .Where(t => t != IModuleType) + .Where(t => !t.IsAbstract) + .Select(type => CreateModuleInfo(type))); + } + + private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory) + { + Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault( + asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase)); + if (loadedAssembly != null) + { + 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); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + internal void LoadAssemblies(IEnumerable assemblies) + { + foreach (string assemblyPath in assemblies) + { + try + { + Assembly.ReflectionOnlyLoadFrom(assemblyPath); + } + catch (FileNotFoundException) + { + // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain + } + } + } + + private static ModuleInfo CreateModuleInfo(Type type) + { + string moduleName = type.Name; + List dependsOn = new List(); + bool onDemand = false; + var moduleAttribute = + CustomAttributeData.GetCustomAttributes(type).FirstOrDefault( + cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName); + + if (moduleAttribute != null) + { + foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments) + { + string argumentName = argument.MemberInfo.Name; + switch (argumentName) + { + case "ModuleName": + moduleName = (string)argument.TypedValue.Value; + break; + + case "OnDemand": + onDemand = (bool)argument.TypedValue.Value; + break; + + case "StartupLoaded": + onDemand = !((bool)argument.TypedValue.Value); + break; + } + } + } + + var moduleDependencyAttributes = + CustomAttributeData.GetCustomAttributes(type).Where( + cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName); + + foreach (CustomAttributeData cad in moduleDependencyAttributes) + { + dependsOn.Add((string)cad.ConstructorArguments[0].Value); + } + + ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName) + { + InitializationMode = onDemand ? InitializationMode.OnDemand : InitializationMode.WhenAvailable, + Ref = type.Assembly.EscapedCodeBase, + }; + + moduleInfo.DependsOn.AddRange(dependsOn); + return moduleInfo; + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs new file mode 100644 index 0000000000..5bdf0dde36 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Prism.Modularity +{ + /// + /// Loads modules from an arbitrary location on the filesystem. This typeloader is only called if + /// classes have a Ref parameter that starts with "file://". + /// This class is only used on the Desktop version of the Prism Library. + /// + public class FileModuleTypeLoader : IModuleTypeLoader, IDisposable + { + private const string RefFilePrefix = "file://"; + + private readonly IAssemblyResolver assemblyResolver; + private HashSet downloadedUris = new HashSet(); + + /// + /// Initializes a new instance of the class. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This is disposed of in the Dispose method.")] + public FileModuleTypeLoader() + : this(new AssemblyResolver()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The assembly resolver. + public FileModuleTypeLoader(IAssemblyResolver assemblyResolver) + { + this.assemblyResolver = assemblyResolver; + } + + /// + /// Raised repeatedly to provide progress as modules are loaded in the background. + /// + public event EventHandler ModuleDownloadProgressChanged; + + private void RaiseModuleDownloadProgressChanged(IModuleInfo moduleInfo, long bytesReceived, long totalBytesToReceive) + { + this.RaiseModuleDownloadProgressChanged(new ModuleDownloadProgressChangedEventArgs(moduleInfo, bytesReceived, totalBytesToReceive)); + } + + private void RaiseModuleDownloadProgressChanged(ModuleDownloadProgressChangedEventArgs e) + { + ModuleDownloadProgressChanged?.Invoke(this, e); + } + + /// + /// Raised when a module is loaded or fails to load. + /// + public event EventHandler LoadModuleCompleted; + + private void RaiseLoadModuleCompleted(IModuleInfo moduleInfo, Exception error) + { + this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, error)); + } + + private void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) + { + this.LoadModuleCompleted?.Invoke(this, e); + } + + /// + /// Evaluates the property to see if the current typeloader will be able to retrieve the . + /// Returns true if the property starts with "file://", because this indicates that the file + /// is a local file. + /// + /// Module that should have it's type loaded. + /// + /// if the current typeloader is able to retrieve the module, otherwise . + /// + /// An is thrown if is null. + public bool CanLoadModuleType(IModuleInfo moduleInfo) + { + if (moduleInfo == null) + { + throw new ArgumentNullException(nameof(moduleInfo)); + } + + return moduleInfo.Ref != null && moduleInfo.Ref.StartsWith(RefFilePrefix, StringComparison.Ordinal); + } + + /// + /// Retrieves the . + /// + /// Module that should have it's type loaded. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is rethrown as part of a completion event")] + public void LoadModuleType(IModuleInfo moduleInfo) + { + if (moduleInfo == null) + { + throw new ArgumentNullException(nameof(moduleInfo)); + } + + try + { + Uri uri = new Uri(moduleInfo.Ref, UriKind.RelativeOrAbsolute); + + // If this module has already been downloaded, I fire the completed event. + if (this.IsSuccessfullyDownloaded(uri)) + { + this.RaiseLoadModuleCompleted(moduleInfo, null); + } + else + { + string path = uri.LocalPath; + + long fileSize = -1L; + if (File.Exists(path)) + { + FileInfo fileInfo = new FileInfo(path); + fileSize = fileInfo.Length; + } + + // Although this isn't asynchronous, nor expected to take very long, I raise progress changed for consistency. + this.RaiseModuleDownloadProgressChanged(moduleInfo, 0, fileSize); + + this.assemblyResolver.LoadAssemblyFrom(moduleInfo.Ref); + + // Although this isn't asynchronous, nor expected to take very long, I raise progress changed for consistency. + this.RaiseModuleDownloadProgressChanged(moduleInfo, fileSize, fileSize); + + // I remember the downloaded URI. + this.RecordDownloadSuccess(uri); + + this.RaiseLoadModuleCompleted(moduleInfo, null); + } + } + catch (Exception ex) + { + this.RaiseLoadModuleCompleted(moduleInfo, ex); + } + } + + private bool IsSuccessfullyDownloaded(Uri uri) + { + lock (this.downloadedUris) + { + return this.downloadedUris.Contains(uri); + } + } + + private void RecordDownloadSuccess(Uri uri) + { + lock (this.downloadedUris) + { + this.downloadedUris.Add(uri); + } + } + + #region Implementation of IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Calls . + /// 2 + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the associated . + /// + /// When , it is being called from the Dispose method. + protected virtual void Dispose(bool disposing) + { + if (this.assemblyResolver is IDisposable disposableResolver) + { + disposableResolver.Dispose(); + } + } + + #endregion + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs new file mode 100644 index 0000000000..e0c3c9b9c9 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs @@ -0,0 +1,14 @@ +namespace Prism.Modularity +{ + /// + /// Interface for classes that are responsible for resolving and loading assembly files. + /// + public interface IAssemblyResolver + { + /// + /// Load an assembly when it's required by the application. + /// + /// + void LoadAssemblyFrom(string assemblyFilePath); + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs new file mode 100644 index 0000000000..d6e3cc91dc --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs @@ -0,0 +1,14 @@ +namespace Prism.Modularity +{ + /// + /// Defines a store for the module metadata. + /// + public interface IConfigurationStore + { + /// + /// Gets the module configuration data. + /// + /// A instance. + ModulesConfigurationSection RetrieveModuleConfigurationSection(); + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs b/src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs new file mode 100644 index 0000000000..8c999a08e1 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs @@ -0,0 +1,187 @@ +using System; +using Prism.Properties; + +namespace Prism.Modularity +{ + /// + /// extensions. + /// + public static class IModuleCatalogExtensions + { + /// + /// Adds the module to the . + /// + /// The catalog to add the module to. + /// The to use. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The type parameter. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, InitializationMode mode = InitializationMode.WhenAvailable, params string[] dependsOn) + where T : IModule + { + return catalog.AddModule(typeof(T).Name, mode, dependsOn); + } + + /// + /// Adds the module to the . + /// + /// The catalog to add the module to. + /// Name of the module to be added. + /// The to use. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The type parameter. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, string name, InitializationMode mode = InitializationMode.WhenAvailable, params string[] dependsOn) + where T : IModule + { + return catalog.AddModule(name, typeof(T).AssemblyQualifiedName, mode, dependsOn); + } + + /// + /// Adds a groupless to the catalog. + /// + /// The catalog to add the module to. + /// of the module to be added. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, Type moduleType, params string[] dependsOn) + { + return catalog.AddModule(moduleType, InitializationMode.WhenAvailable, dependsOn); + } + + /// + /// Adds a groupless to the catalog. + /// + /// The catalog to add the module to. + /// of the module to be added. + /// Stage on which the module to be added will be initialized. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, Type moduleType, InitializationMode initializationMode, params string[] dependsOn) + { + if (moduleType == null) + throw new ArgumentNullException(nameof(moduleType)); + + return catalog.AddModule(moduleType.Name, moduleType.AssemblyQualifiedName, initializationMode, dependsOn); + } + + /// + /// Adds a groupless to the catalog. + /// + /// The catalog to add the module to. + /// Name of the module to be added. + /// of the module to be added. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, string moduleName, string moduleType, params string[] dependsOn) + { + return catalog.AddModule(moduleName, moduleType, InitializationMode.WhenAvailable, dependsOn); + } + + /// + /// Adds a groupless to the catalog. + /// + /// The catalog to add the module to. + /// Name of the module to be added. + /// of the module to be added. + /// Stage on which the module to be added will be initialized. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, string moduleName, string moduleType, InitializationMode initializationMode, params string[] dependsOn) + { + return catalog.AddModule(moduleName, moduleType, null, initializationMode, dependsOn); + } + + /// + /// Adds a groupless to the catalog. + /// + /// The catalog to add the module to. + /// Name of the module to be added. + /// of the module to be added. + /// Reference to the location of the module to be added assembly. + /// Stage on which the module to be added will be initialized. + /// Collection of module names () of the modules on which the module to be added logically depends on. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, string moduleName, string moduleType, string refValue, InitializationMode initializationMode, params string[] dependsOn) + { + if (moduleName == null) + throw new ArgumentNullException(nameof(moduleName)); + + if (moduleType == null) + throw new ArgumentNullException(nameof(moduleType)); + + ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleType, dependsOn) + { + InitializationMode = initializationMode, + Ref = refValue + }; + return catalog.AddModule(moduleInfo); + } + + /// + /// Adds the module to the . + /// + /// The catalog to add the module to. + /// The to use. + /// The type parameter. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, InitializationMode mode = InitializationMode.WhenAvailable) + where T : IModule => + catalog.AddModule(typeof(T).Name, mode); + + /// + /// Adds the module to the . + /// + /// The catalog to add the module to. + /// Name of the module to be added. + /// The type parameter. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, string name) + where T : IModule => + catalog.AddModule(name, InitializationMode.WhenAvailable); + + /// + /// Adds the module to the . + /// + /// The catalog to add the module to. + /// Name of the module to be added. + /// The to use. + /// The type parameter. + /// The same instance with the added module. + public static IModuleCatalog AddModule(this IModuleCatalog catalog, string name, InitializationMode mode) + where T : IModule => + catalog.AddModule(new ModuleInfo(typeof(T), name, mode)); + + /// + /// Creates and adds a to the catalog. + /// + /// The catalog to add the module to. + /// Stage on which the module group to be added will be initialized. + /// Reference to the location of the module group to be added. + /// Collection of included in the group. + /// The same with the added module group. + public static IModuleCatalog AddGroup(this IModuleCatalog catalog, InitializationMode initializationMode, string refValue, params ModuleInfo[] moduleInfos) + { + if (!(catalog is IModuleGroupsCatalog groupSupport)) + throw new NotSupportedException(Resources.MustBeModuleGroupCatalog); + + if (moduleInfos == null) + throw new ArgumentNullException(nameof(moduleInfos)); + + ModuleInfoGroup newGroup = new ModuleInfoGroup + { + InitializationMode = initializationMode, + Ref = refValue + }; + + foreach (var info in moduleInfos) + { + newGroup.Add(info); + } + + groupSupport.Items.Add(newGroup); + + return catalog; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs b/src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs new file mode 100644 index 0000000000..ac1c7b30fc --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; + +namespace Prism.Modularity +{ + /// + /// Defines a model that can get the collection of . + /// + public interface IModuleGroupsCatalog + { + /// + /// Gets the items in the . This property is mainly used to add s or + /// s through XAML. + /// + /// The items in the catalog. + Collection Items { get; } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs b/src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs new file mode 100644 index 0000000000..a22ac48b42 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs @@ -0,0 +1,36 @@ +using System; + +namespace Prism.Modularity +{ + /// + /// Defines the interface for moduleTypeLoaders + /// + public interface IModuleTypeLoader + { + /// + /// Evaluates the property to see if the current typeloader will be able to retrieve the . + /// + /// Module that should have it's type loaded. + /// if the current typeloader is able to retrieve the module, otherwise . + bool CanLoadModuleType(IModuleInfo moduleInfo); + + /// + /// Retrieves the . + /// + /// Module that should have it's type loaded. + void LoadModuleType(IModuleInfo moduleInfo); + + /// + /// Raised repeatedly to provide progress as modules are downloaded in the background. + /// + event EventHandler ModuleDownloadProgressChanged; + + /// + /// Raised when a module is loaded or fails to load. + /// + /// + /// This event is raised once per ModuleInfo instance requested in . + /// + event EventHandler LoadModuleCompleted; + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs new file mode 100644 index 0000000000..e62a2905b9 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs @@ -0,0 +1,25 @@ +using System; + +namespace Prism.Modularity +{ + /// + /// Indicates that the class should be considered a named module using the + /// provided module name. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class ModuleAttribute : Attribute + { + /// + /// Gets or sets the name of the module. + /// + /// The name of the module. + public string ModuleName { get; set; } + + /// + /// Gets or sets the value indicating whether the module should be loaded OnDemand. + /// + /// When (default value), it indicates the module should be loaded as soon as it's dependencies are satisfied. + /// Otherwise you should explicitly load this module via the . + public bool OnDemand { get; set; } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs new file mode 100644 index 0000000000..a358c79f8b --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using Avalonia.Metadata; + +namespace Prism.Modularity +{ + /// + /// The holds information about the modules that can be used by the + /// application. Each module is described in a class, that records the + /// name, type and location of the module. + /// + /// It also verifies that the is internally valid. That means that + /// it does not have: + /// + /// Circular dependencies + /// Missing dependencies + /// + /// Invalid dependencies, such as a Module that's loaded at startup that depends on a module + /// that might need to be retrieved. + /// + /// + /// The also serves as a baseclass for more specialized Catalogs . + /// + ////[ContentProperty("Items")] // Avalonia does use, System.Windows.Markup. See property `Items` below. + public class ModuleCatalog : ModuleCatalogBase, IModuleGroupsCatalog + { + /// + /// Initializes a new instance of the class. + /// + public ModuleCatalog() : base() + { + } + + /// + /// Initializes a new instance of the class while providing an + /// initial list of s. + /// + /// The initial list of modules. + public ModuleCatalog(IEnumerable modules) : base(modules) + { + } + + /// + /// 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; + + /// + /// Creates a valid file uri to locate the module assembly file + /// + /// The relative path to the file + /// The valid absolute file path + protected virtual string GetFileAbsoluteUri(string filePath) + { + UriBuilder uriBuilder = new UriBuilder(); + uriBuilder.Host = String.Empty; + uriBuilder.Scheme = Uri.UriSchemeFile; + uriBuilder.Path = Path.GetFullPath(filePath); + Uri fileUri = uriBuilder.Uri; + + return fileUri.ToString(); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs new file mode 100644 index 0000000000..189f0f1a14 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs @@ -0,0 +1,88 @@ +using System.Configuration; + +namespace Prism.Modularity +{ + /// + /// A configuration element to declare module metadata. + /// + public class ModuleConfigurationElement : ConfigurationElement + { + /// + /// Initializes a new instance of . + /// + public ModuleConfigurationElement() + { + } + + /// + /// Initializes a new instance of . + /// + /// The assembly file where the module is located. + /// The type of the module. + /// The name of the module. + /// This attribute specifies whether the module is loaded at startup. + public ModuleConfigurationElement(string assemblyFile, string moduleType, string moduleName, bool startupLoaded) + { + base["assemblyFile"] = assemblyFile; + base["moduleType"] = moduleType; + base["moduleName"] = moduleName; + base["startupLoaded"] = startupLoaded; + } + + /// + /// Gets or sets the assembly file. + /// + /// The assembly file. + [ConfigurationProperty("assemblyFile", IsRequired = true)] + public string AssemblyFile + { + get { return (string)base["assemblyFile"]; } + set { base["assemblyFile"] = value; } + } + + /// + /// Gets or sets the module type. + /// + /// The module's type. + [ConfigurationProperty("moduleType", IsRequired = true)] + public string ModuleType + { + get { return (string)base["moduleType"]; } + set { base["moduleType"] = value; } + } + + /// + /// Gets or sets the module name. + /// + /// The module's name. + [ConfigurationProperty("moduleName", IsRequired = true)] + public string ModuleName + { + get { return (string)base["moduleName"]; } + set { base["moduleName"] = value; } + } + + /// + /// Gets or sets a value indicating whether the module should be loaded at startup. + /// + /// A value indicating whether the module should be loaded at startup. + [ConfigurationProperty("startupLoaded", IsRequired = false, DefaultValue = true)] + public bool StartupLoaded + { + get { return (bool)base["startupLoaded"]; } + set { base["startupLoaded"] = value; } + } + + /// + /// Gets or sets the modules this module depends on. + /// + /// The names of the modules that this depends on. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] + [ConfigurationProperty("dependencies", IsDefaultCollection = true, IsKey = false)] + public ModuleDependencyCollection Dependencies + { + get { return (ModuleDependencyCollection)base["dependencies"]; } + set { base["dependencies"] = value; } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs new file mode 100644 index 0000000000..f7af7ede5d --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Configuration; + +namespace Prism.Modularity +{ + /// + /// A collection of . + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")] + public class ModuleConfigurationElementCollection : ConfigurationElementCollection + { + /// + /// Initializes a new instance of . + /// + public ModuleConfigurationElementCollection() + { + } + + /// + /// Initializes a new . + /// + /// The initial set of . + /// An is thrown if is . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] + public ModuleConfigurationElementCollection(ModuleConfigurationElement[] modules) + { + if (modules == null) + throw new ArgumentNullException(nameof(modules)); + + foreach (ModuleConfigurationElement module in modules) + { + BaseAdd(module); + } + } + + /// + /// Gets a value indicating whether an exception should be raised if a duplicate element is found. + /// This property will always return true. + /// + /// A value. + protected override bool ThrowOnDuplicate + { + get { return true; } + } + + /// + ///Gets the type of the . + /// + /// + ///The of this collection. + /// + public override ConfigurationElementCollectionType CollectionType + { + get { return ConfigurationElementCollectionType.BasicMap; } + } + + /// + ///Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class. + /// + /// + ///The name of the collection; otherwise, an empty string. + /// + protected override string ElementName + { + get { return "module"; } + } + + /// + /// Gets the located at the specified index in the collection. + /// + /// The index of the element in the collection. + /// A . + public ModuleConfigurationElement this[int index] + { + get { return (ModuleConfigurationElement)base.BaseGet(index); } + } + + /// + /// Adds a to the collection. + /// + /// A instance. + public void Add(ModuleConfigurationElement module) + { + BaseAdd(module); + } + + /// + /// Tests if the collection contains the configuration for the specified module name. + /// + /// The name of the module to search the configuration for. + /// if a configuration for the module is present; otherwise . + public bool Contains(string moduleName) + { + return base.BaseGet(moduleName) != null; + } + + /// + /// Searches the collection for all the that match the specified predicate. + /// + /// A that implements the match test. + /// A with the successful matches. + /// An is thrown if is null. + public IList FindAll(Predicate match) + { + if (match == null) + throw new ArgumentNullException(nameof(match)); + + IList found = new List(); + foreach (ModuleConfigurationElement moduleElement in this) + { + if (match(moduleElement)) + { + found.Add(moduleElement); + } + } + return found; + } + + /// + /// Creates a new . + /// + /// A . + protected override ConfigurationElement CreateNewElement() + { + return new ModuleConfigurationElement(); + } + + /// + /// Gets the element key for a specified configuration element when overridden in a derived class. + /// + /// The to return the key for. + /// + /// An that acts as the key for the specified . + /// + protected override object GetElementKey(ConfigurationElement element) + { + return ((ModuleConfigurationElement)element).ModuleName; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs new file mode 100644 index 0000000000..112ace84fb --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs @@ -0,0 +1,88 @@ +using System; +using System.Configuration; + +namespace Prism.Modularity +{ + /// + /// A collection of . + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")] + public class ModuleDependencyCollection : ConfigurationElementCollection + { + /// + /// Initializes a new instance of . + /// + public ModuleDependencyCollection() + { + } + + /// + /// Initializes a new instance of . + /// + /// An array of with initial list of dependencies. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] + public ModuleDependencyCollection(ModuleDependencyConfigurationElement[] dependencies) + { + if (dependencies == null) + throw new ArgumentNullException(nameof(dependencies)); + + foreach (ModuleDependencyConfigurationElement dependency in dependencies) + { + BaseAdd(dependency); + } + } + + /// + ///Gets the type of the . + /// + /// + ///The of this collection. + /// + public override ConfigurationElementCollectionType CollectionType + { + get { return ConfigurationElementCollectionType.BasicMap; } + } + + /// + ///Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class. + /// + /// + ///The name of the collection; otherwise, an empty string. + /// + protected override string ElementName + { + get { return "dependency"; } + } + + /// + /// Gets the located at the specified index in the collection. + /// + /// The index of the element in the collection. + /// A . + public ModuleDependencyConfigurationElement this[int index] + { + get { return (ModuleDependencyConfigurationElement)base.BaseGet(index); } + } + + /// + /// Creates a new . + /// + /// A . + protected override ConfigurationElement CreateNewElement() + { + return new ModuleDependencyConfigurationElement(); + } + + /// + ///Gets the element key for a specified configuration element when overridden in a derived class. + /// + ///The to return the key for. + /// + ///An that acts as the key for the specified . + /// + protected override object GetElementKey(ConfigurationElement element) + { + return ((ModuleDependencyConfigurationElement)element).ModuleName; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs new file mode 100644 index 0000000000..a6e486e50b --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs @@ -0,0 +1,37 @@ +using System.Configuration; + +namespace Prism.Modularity +{ + /// + /// A for module dependencies. + /// + public class ModuleDependencyConfigurationElement : ConfigurationElement + { + /// + /// Initializes a new instance of . + /// + public ModuleDependencyConfigurationElement() + { + } + + /// + /// Initializes a new instance of . + /// + /// A module name. + public ModuleDependencyConfigurationElement(string moduleName) + { + base["moduleName"] = moduleName; + } + + /// + /// Gets or sets the name of a module another module depends on. + /// + /// The name of a module another module depends on. + [ConfigurationProperty("moduleName", IsRequired = true, IsKey = true)] + public string ModuleName + { + get { return (string)base["moduleName"]; } + set { base["moduleName"] = value; } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs new file mode 100644 index 0000000000..ba324a0bcf --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs @@ -0,0 +1,9 @@ +using System; + +namespace Prism.Modularity +{ + [Serializable] + public partial class ModuleInfo + { + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs new file mode 100644 index 0000000000..62fb28c628 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.ObjectModel; + +namespace Prism.Modularity +{ + /// + /// Defines the metadata that describes a module. + /// + public partial class ModuleInfo : IModuleInfo + { + /// + /// Initializes a new empty instance of . + /// + public ModuleInfo() + : this(null, null, new string[0]) + { + } + + /// + /// Initializes a new instance of . + /// + /// The module's name. + /// The module 's AssemblyQualifiedName. + /// The modules this instance depends on. + /// An is thrown if is . + public ModuleInfo(string name, string type, params string[] dependsOn) + { + if (dependsOn == null) + throw new ArgumentNullException(nameof(dependsOn)); + + this.ModuleName = name; + this.ModuleType = type; + this.DependsOn = new Collection(); + foreach (string dependency in dependsOn) + { + this.DependsOn.Add(dependency); + } + } + + /// + /// Initializes a new instance of . + /// + /// The module's name. + /// The module's type. + public ModuleInfo(string name, string type) : this(name, type, new string[0]) + { + } + + /// + /// Initializes a new instance of . + /// + /// The module's type. + public ModuleInfo(Type moduleType) + : this(moduleType, moduleType.Name) { } + + /// + /// Initializes a new instance of . + /// + /// The module's type. + /// The module's name. + public ModuleInfo(Type moduleType, string moduleName) + : this(moduleType, moduleName, InitializationMode.WhenAvailable) { } + + /// + /// Initializes a new instance of . + /// + /// The module's type. + /// The module's name. + /// The module's . + public ModuleInfo(Type moduleType, string moduleName, InitializationMode initializationMode) + : this(moduleName, moduleType.AssemblyQualifiedName) + { + InitializationMode = initializationMode; + } + + /// + /// Gets or sets the name of the module. + /// + /// The name of the module. + public string ModuleName { get; set; } + + /// + /// Gets or sets the module 's AssemblyQualifiedName. + /// + /// The type of the module. + public string ModuleType { get; set; } + + /// + /// Gets or sets the list of modules that this module depends upon. + /// + /// The list of modules that this module depends upon. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The setter is here to work around a Silverlight issue with setting properties from within Xaml.")] + public Collection DependsOn { get; set; } + + /// + /// Specifies on which stage the Module will be initialized. + /// + public InitializationMode InitializationMode { get; set; } + + /// + /// Reference to the location of the module assembly. + /// The following are examples of valid values: + /// file://c:/MyProject/Modules/MyModule.dll for a loose DLL in WPF. + /// + /// + public string Ref { get; set; } + + /// + /// Gets or sets the state of the with regards to the module loading and initialization process. + /// + public ModuleState State { get; set; } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs new file mode 100644 index 0000000000..82a1031345 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Prism.Properties; + +namespace Prism.Modularity +{ + /// + /// Represents a group of instances that are usually deployed together. s + /// are also used by the to prevent common deployment problems such as having a module that's required + /// at startup that depends on modules that will only be downloaded on demand. + /// + /// The group also forwards and values to the s that it + /// contains. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + public class ModuleInfoGroup : IModuleInfoGroup + { + private readonly Collection _modules = new Collection(); + + /// + /// Gets or sets the for the whole group. Any classes that are + /// added after setting this value will also get this . + /// + /// + /// The initialization mode. + public InitializationMode InitializationMode { get; set; } + + /// + /// Gets or sets the value for the whole group. Any classes that are + /// added after setting this value will also get this . + /// + /// The ref value will also be used by the to determine which to use. + /// For example, using an "file://" prefix with a valid URL will cause the FileModuleTypeLoader to be used + /// (Only available in the desktop version of CAL). + /// + /// + /// The ref value that will be used. + public string Ref { get; set; } + + /// + /// Adds an moduleInfo to the . + /// + /// The to the . + public void Add(IModuleInfo item) + { + ForwardValues(item); + _modules.Add(item); + } + + internal void UpdateModulesRef() + { + foreach (var module in _modules) + { + module.Ref = Ref; + } + } + + /// + /// Forwards and properties from this + /// to . + /// + /// The module info to forward values to. + /// An is thrown if is . + protected void ForwardValues(IModuleInfo moduleInfo) + { + if (moduleInfo == null) + throw new ArgumentNullException(nameof(moduleInfo)); + + if (moduleInfo.Ref == null) + { + moduleInfo.Ref = Ref; + } + + if (moduleInfo.InitializationMode == InitializationMode.WhenAvailable && InitializationMode != InitializationMode.WhenAvailable) + { + moduleInfo.InitializationMode = InitializationMode; + } + } + + /// + /// Removes all s from the . + /// + public void Clear() => _modules.Clear(); + + /// + /// Determines whether the contains a specific value. + /// + /// The object to locate in the . + /// + /// true if is found in the ; otherwise, false. + /// + public bool Contains(IModuleInfo item) => _modules.Contains(item); + + /// + /// Copies the elements of the to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// + /// is null. + /// + /// + /// is less than 0. + /// + /// + /// is multidimensional. + /// -or- + /// is equal to or greater than the length of . + /// -or- + /// The number of elements in the source is greater than the available space from to the end of the destination . + /// + public void CopyTo(IModuleInfo[] array, int arrayIndex) + { + _modules.CopyTo(array, arrayIndex); + } + + /// + /// Gets the number of elements contained in the . + /// + /// + /// + /// The number of elements contained in the . + /// + public int Count => _modules.Count; + + /// + /// Gets a value indicating whether the is read-only. + /// + /// + /// false, because the is not Read-Only. + /// + public bool IsReadOnly => false; + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// The object to remove from the . + /// + /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . + /// + public bool Remove(IModuleInfo item) => _modules.Remove(item); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() => _modules.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + /// Adds an item to the . + /// + /// + /// The to add to the . + /// Must be of type + /// + /// + /// The position into which the new element was inserted. + /// + int IList.Add(object value) + { + this.Add((IModuleInfo)value); + return 1; + } + + /// + /// Determines whether the contains a specific value. + /// + /// + /// The to locate in the . + /// Must be of type + /// + /// + /// true if the is found in the ; otherwise, false. + /// + 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)); + + return Contains(moduleInfo); + } + + /// + /// Determines the index of a specific item in the . + /// + /// + /// The to locate in the . + /// Must be of type + /// + /// + /// The index of if found in the list; otherwise, -1. + /// + public int IndexOf(object value) => _modules.IndexOf((IModuleInfo)value); + + /// + /// Inserts an item to the at the specified index. + /// + /// The zero-based index at which should be inserted. + /// + /// The to insert into the . + /// Must be of type + /// + /// + /// is not a valid index in the . + /// + /// + /// If is null. + /// + /// + /// If is not of type + /// + 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)); + + _modules.Insert(index, moduleInfo); + } + + /// + /// Gets a value indicating whether the has a fixed size. + /// + /// false, because the does not have a fixed length. + /// + public bool IsFixedSize => false; + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// + /// The to remove from the . + /// Must be of type + /// + void IList.Remove(object value) + { + Remove((IModuleInfo)value); + } + + /// + /// Removes the item at the specified index. + /// + /// The zero-based index of the item to remove. + /// + /// is not a valid index in the . + /// + /// + /// The is read-only. + /// + public void RemoveAt(int index) => _modules.RemoveAt(index); + + /// + /// Gets or sets the at the specified index. + /// + /// + object IList.this[int index] + { + get => this[index]; + set => this[index] = (ModuleInfo)value; + } + + /// + /// Copies the elements of the to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// + /// is null. + /// + /// + /// is less than zero. + /// + /// + /// is multidimensional. + /// -or- + /// is equal to or greater than the length of . + /// -or- + /// The number of elements in the source is greater than the available space from to the end of the destination . + /// + /// + /// The type of the source cannot be cast automatically to the type of the destination . + /// + void ICollection.CopyTo(Array array, int index) => + ((ICollection)_modules).CopyTo(array, index); + + /// + /// Gets a value indicating whether access to the is synchronized (thread safe). + /// + /// + /// true if access to the is synchronized (thread safe); otherwise, false. + /// + public bool IsSynchronized => ((ICollection)_modules).IsSynchronized; + + /// + /// Gets an object that can be used to synchronize access to the . + /// + /// + /// + /// An object that can be used to synchronize access to the . + /// + public object SyncRoot => ((ICollection)_modules).SyncRoot; + + /// + /// Determines the index of a specific item in the . + /// + /// The object to locate in the . + /// + /// The index of if found in the list; otherwise, -1. + /// + public int IndexOf(IModuleInfo item) => _modules.IndexOf(item); + + /// + /// Inserts an item to the at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The object to insert into the . + /// + /// is not a valid index in the . + /// + public void Insert(int index, IModuleInfo item) => _modules.Insert(index, item); + + /// + /// Gets or sets the at the specified index. + /// + /// The at the specified index + public IModuleInfo this[int index] + { + get => _modules[index]; + set => _modules[index] = value; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs new file mode 100644 index 0000000000..2813965598 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.ObjectModel; + +namespace Prism.Modularity +{ + /// + /// Defines extension methods for the class. + /// + public static class ModuleInfoGroupExtensions + { + /// + /// Adds a new module that is statically referenced to the specified module info group. + /// + /// The group where to add the module info in. + /// The name for the module. + /// The type for the module. This type should be a descendant of . + /// The names for the modules that this module depends on. + /// Returns the instance of the passed in module info group, to provide a fluid interface. + public static ModuleInfoGroup AddModule( + this ModuleInfoGroup moduleInfoGroup, + string moduleName, + Type moduleType, + params string[] dependsOn) + { + if (moduleType == null) + throw new ArgumentNullException(nameof(moduleType)); + + if (moduleInfoGroup == null) + throw new ArgumentNullException(nameof(moduleInfoGroup)); + + ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleType.AssemblyQualifiedName); + moduleInfo.DependsOn.AddRange(dependsOn); + moduleInfoGroup.Add(moduleInfo); + return moduleInfoGroup; + } + + /// + /// Adds a new module that is statically referenced to the specified module info group. + /// + /// The group where to add the module info in. + /// The type for the module. This type should be a descendant of . + /// The names for the modules that this module depends on. + /// Returns the instance of the passed in module info group, to provide a fluid interface. + /// The name of the module will be the type name. + public static ModuleInfoGroup AddModule( + this ModuleInfoGroup moduleInfoGroup, + Type moduleType, + params string[] dependsOn) + { + if (moduleType == null) + throw new ArgumentNullException(nameof(moduleType)); + + return AddModule(moduleInfoGroup, moduleType.Name, moduleType, dependsOn); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs new file mode 100644 index 0000000000..5047fd8829 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs @@ -0,0 +1,118 @@ +using System; +using System.Globalization; +using Prism.Ioc; + +namespace Prism.Modularity +{ + /// + /// Implements the interface. Handles loading of a module based on a type. + /// + public class ModuleInitializer : IModuleInitializer + { + private readonly IContainerExtension _containerExtension; + + /// + /// Initializes a new instance of . + /// + /// The container that will be used to resolve the modules by specifying its type. + public ModuleInitializer(IContainerExtension containerExtension) + { + this._containerExtension = containerExtension ?? throw new ArgumentNullException(nameof(containerExtension)); + } + + /// + /// Initializes the specified module. + /// + /// The module to initialize + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catches Exception to handle any exception thrown during the initialization process with the HandleModuleInitializationError method.")] + public void Initialize(IModuleInfo moduleInfo) + { + if (moduleInfo == null) + throw new ArgumentNullException(nameof(moduleInfo)); + + IModule moduleInstance = null; + try + { + moduleInstance = this.CreateModule(moduleInfo); + if (moduleInstance != null) + { + moduleInstance.RegisterTypes(_containerExtension); + moduleInstance.OnInitialized(_containerExtension); + } + } + catch (Exception ex) + { + this.HandleModuleInitializationError( + moduleInfo, + moduleInstance?.GetType().Assembly.FullName, + ex); + } + } + + /// + /// Handles any exception occurred in the module Initialization process, + /// This method can be overridden to provide a different behavior. + /// + /// The module metadata where the error happened. + /// The assembly name. + /// The exception thrown that is the cause of the current error. + /// + public virtual void HandleModuleInitializationError(IModuleInfo moduleInfo, string assemblyName, Exception exception) + { + if (moduleInfo == null) + throw new ArgumentNullException(nameof(moduleInfo)); + + if (exception == null) + throw new ArgumentNullException(nameof(exception)); + + Exception moduleException; + + if (exception is ModuleInitializeException) + { + moduleException = exception; + } + else + { + if (!string.IsNullOrEmpty(assemblyName)) + { + moduleException = new ModuleInitializeException(moduleInfo.ModuleName, assemblyName, exception.Message, exception); + } + else + { + moduleException = new ModuleInitializeException(moduleInfo.ModuleName, exception.Message, exception); + } + } + + throw moduleException; + } + + /// + /// Uses the container to resolve a new by specifying its . + /// + /// The module to create. + /// A new instance of the module specified by . + protected virtual IModule CreateModule(IModuleInfo moduleInfo) + { + if (moduleInfo == null) + throw new ArgumentNullException(nameof(moduleInfo)); + + return this.CreateModule(moduleInfo.ModuleType); + } + + /// + /// Uses the container to resolve a new by specifying its . + /// + /// The type name to resolve. This type must implement . + /// A new instance of . + protected virtual IModule CreateModule(string typeName) + { + Type moduleType = Type.GetType(typeName); + if (moduleType == null) + { + throw new ModuleInitializeException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FailedToGetType, typeName)); + } + + return (IModule)_containerExtension.Resolve(moduleType); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs new file mode 100644 index 0000000000..3d2a3f0a52 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Prism.Modularity +{ + /// + /// 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. + /// + /// The module type loaders. + public virtual IEnumerable ModuleTypeLoaders + { + get + { + if (this.typeLoaders == null) + { + this.typeLoaders = new List + { + new FileModuleTypeLoader() + }; + } + + return this.typeLoaders; + } + + set + { + this.typeLoaders = value; + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs new file mode 100644 index 0000000000..f27ec26d6d --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Prism.Properties; + +namespace Prism.Modularity +{ + /// + /// Component responsible for coordinating the modules' type loading and module initialization process. + /// + public partial class ModuleManager : IModuleManager, IDisposable + { + private readonly IModuleInitializer moduleInitializer; + private IEnumerable typeLoaders; + private HashSet subscribedToModuleTypeLoaders = new HashSet(); + + /// + /// Initializes an instance of the class. + /// + /// Service used for initialization of modules. + /// Catalog that enumerates the modules to be loaded and initialized. + public ModuleManager(IModuleInitializer moduleInitializer, IModuleCatalog moduleCatalog) + { + this.moduleInitializer = moduleInitializer ?? throw new ArgumentNullException(nameof(moduleInitializer)); + ModuleCatalog = moduleCatalog ?? throw new ArgumentNullException(nameof(moduleCatalog)); + } + + /// + /// The module catalog specified in the constructor. + /// + protected IModuleCatalog ModuleCatalog { get; } + + /// + /// Gets all the classes that are in the . + /// + public IEnumerable Modules => ModuleCatalog.Modules; + + /// + /// Raised repeatedly to provide progress as modules are loaded in the background. + /// + public event EventHandler ModuleDownloadProgressChanged; + + private void RaiseModuleDownloadProgressChanged(ModuleDownloadProgressChangedEventArgs e) + { + ModuleDownloadProgressChanged?.Invoke(this, e); + } + + /// + /// Raised when a module is loaded or fails to load. + /// + public event EventHandler LoadModuleCompleted; + + private void RaiseLoadModuleCompleted(IModuleInfo moduleInfo, Exception error) + { + this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, error)); + } + + private void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) + { + this.LoadModuleCompleted?.Invoke(this, e); + } + + /// + /// Initializes the modules marked as on the . + /// + public void Run() + { + this.ModuleCatalog.Initialize(); + + this.LoadModulesWhenAvailable(); + } + + + /// + /// Loads and initializes the module on the with the name . + /// + /// Name of the module requested for initialization. + public void LoadModule(string moduleName) + { + var module = this.ModuleCatalog.Modules.Where(m => m.ModuleName == moduleName); + if (module == null || module.Count() != 1) + { + throw new ModuleNotFoundException(moduleName, string.Format(CultureInfo.CurrentCulture, Resources.ModuleNotFound, moduleName)); + } + + var modulesToLoad = this.ModuleCatalog.CompleteListWithDependencies(module); + + this.LoadModuleTypes(modulesToLoad); + } + + /// + /// Checks if the module needs to be retrieved before it's initialized. + /// + /// Module that is being checked if needs retrieval. + /// + protected virtual bool ModuleNeedsRetrieval(IModuleInfo moduleInfo) + { + if (moduleInfo == null) + throw new ArgumentNullException(nameof(moduleInfo)); + + if (moduleInfo.State == ModuleState.NotStarted) + { + // If we can instantiate the type, that means the module's assembly is already loaded into + // the AppDomain and we don't need to retrieve it. + bool isAvailable = Type.GetType(moduleInfo.ModuleType) != null; + if (isAvailable) + { + moduleInfo.State = ModuleState.ReadyForInitialization; + } + + return !isAvailable; + } + + return false; + } + + private void LoadModulesWhenAvailable() + { + var whenAvailableModules = this.ModuleCatalog.Modules.Where(m => m.InitializationMode == InitializationMode.WhenAvailable); + var modulesToLoadTypes = this.ModuleCatalog.CompleteListWithDependencies(whenAvailableModules); + if (modulesToLoadTypes != null) + { + this.LoadModuleTypes(modulesToLoadTypes); + } + } + + private void LoadModuleTypes(IEnumerable moduleInfos) + { + if (moduleInfos == null) + { + return; + } + + foreach (var moduleInfo in moduleInfos) + { + if (moduleInfo.State == ModuleState.NotStarted) + { + if (this.ModuleNeedsRetrieval(moduleInfo)) + { + this.BeginRetrievingModule(moduleInfo); + } + else + { + moduleInfo.State = ModuleState.ReadyForInitialization; + } + } + } + + this.LoadModulesThatAreReadyForLoad(); + } + + /// + /// Loads the modules that are not initialized and have their dependencies loaded. + /// + protected virtual void LoadModulesThatAreReadyForLoad() + { + bool keepLoading = true; + while (keepLoading) + { + keepLoading = false; + var availableModules = this.ModuleCatalog.Modules.Where(m => m.State == ModuleState.ReadyForInitialization); + + foreach (var moduleInfo in availableModules) + { + if ((moduleInfo.State != ModuleState.Initialized) && (this.AreDependenciesLoaded(moduleInfo))) + { + moduleInfo.State = ModuleState.Initializing; + this.InitializeModule(moduleInfo); + keepLoading = true; + break; + } + } + } + } + + private void BeginRetrievingModule(IModuleInfo moduleInfo) + { + var moduleInfoToLoadType = moduleInfo; + IModuleTypeLoader moduleTypeLoader = this.GetTypeLoaderForModule(moduleInfoToLoadType); + moduleInfoToLoadType.State = ModuleState.LoadingTypes; + + // Delegate += works differently between SL and WPF. + // We only want to subscribe to each instance once. + if (!this.subscribedToModuleTypeLoaders.Contains(moduleTypeLoader)) + { + moduleTypeLoader.ModuleDownloadProgressChanged += this.IModuleTypeLoader_ModuleDownloadProgressChanged; + moduleTypeLoader.LoadModuleCompleted += this.IModuleTypeLoader_LoadModuleCompleted; + this.subscribedToModuleTypeLoaders.Add(moduleTypeLoader); + } + + moduleTypeLoader.LoadModuleType(moduleInfo); + } + + private void IModuleTypeLoader_ModuleDownloadProgressChanged(object sender, ModuleDownloadProgressChangedEventArgs e) + { + this.RaiseModuleDownloadProgressChanged(e); + } + + private void IModuleTypeLoader_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e) + { + if (e.Error == null) + { + if ((e.ModuleInfo.State != ModuleState.Initializing) && (e.ModuleInfo.State != ModuleState.Initialized)) + { + e.ModuleInfo.State = ModuleState.ReadyForInitialization; + } + + // This callback may call back on the UI thread, but we are not guaranteeing it. + // If you were to add a custom retriever that retrieved in the background, you + // would need to consider dispatching to the UI thread. + this.LoadModulesThatAreReadyForLoad(); + } + else + { + this.RaiseLoadModuleCompleted(e); + + // If the error is not handled then I log it and raise an exception. + if (!e.IsErrorHandled) + { + this.HandleModuleTypeLoadingError(e.ModuleInfo, e.Error); + } + } + } + + /// + /// Handles any exception occurred in the module typeloading process, + /// and throws a . + /// This method can be overridden to provide a different behavior. + /// + /// The module metadata where the error happened. + /// The exception thrown that is the cause of the current error. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1")] + protected virtual void HandleModuleTypeLoadingError(IModuleInfo moduleInfo, Exception exception) + { + if (moduleInfo == null) + throw new ArgumentNullException(nameof(moduleInfo)); + + + if (!(exception is ModuleTypeLoadingException moduleTypeLoadingException)) + { + moduleTypeLoadingException = new ModuleTypeLoadingException(moduleInfo.ModuleName, exception.Message, exception); + } + + throw moduleTypeLoadingException; + } + + private bool AreDependenciesLoaded(IModuleInfo moduleInfo) + { + var requiredModules = this.ModuleCatalog.GetDependentModules(moduleInfo); + if (requiredModules == null) + { + return true; + } + + int notReadyRequiredModuleCount = + requiredModules.Count(requiredModule => requiredModule.State != ModuleState.Initialized); + + return notReadyRequiredModuleCount == 0; + } + + private IModuleTypeLoader GetTypeLoaderForModule(IModuleInfo moduleInfo) + { + foreach (IModuleTypeLoader typeLoader in this.ModuleTypeLoaders) + { + if (typeLoader.CanLoadModuleType(moduleInfo)) + { + return typeLoader; + } + } + + throw new ModuleTypeLoaderNotFoundException(moduleInfo.ModuleName, string.Format(CultureInfo.CurrentCulture, Resources.NoRetrieverCanRetrieveModule, moduleInfo.ModuleName), null); + } + + private void InitializeModule(IModuleInfo moduleInfo) + { + if (moduleInfo.State == ModuleState.Initializing) + { + this.moduleInitializer.Initialize(moduleInfo); + moduleInfo.State = ModuleState.Initialized; + this.RaiseLoadModuleCompleted(moduleInfo, null); + } + } + + #region Implementation of IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Calls . + /// 2 + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the associated s. + /// + /// When , it is being called from the Dispose method. + protected virtual void Dispose(bool disposing) + { + foreach (IModuleTypeLoader typeLoader in this.ModuleTypeLoaders) + { + if (typeLoader is IDisposable disposableTypeLoader) + { + disposableTypeLoader.Dispose(); + } + } + } + + #endregion + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs new file mode 100644 index 0000000000..fb7e0a919d --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.Serialization; + +namespace Prism.Modularity +{ + [Serializable] + public partial class ModuleTypeLoaderNotFoundException + { + /// + /// Initializes a new instance with serialized data. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected ModuleTypeLoaderNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs new file mode 100644 index 0000000000..fdf7ddd04f --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs @@ -0,0 +1,53 @@ +using System; + +namespace Prism.Modularity +{ + /// + /// Exception that's thrown when there is no registered in + /// that can handle this particular type of module. + /// + public partial class ModuleTypeLoaderNotFoundException : ModularityException + { + /// + /// Initializes a new instance of the class. + /// + public ModuleTypeLoaderNotFoundException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// + /// The message that describes the error. + /// + public ModuleTypeLoaderNotFoundException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// + /// The message that describes the error. + /// + /// The inner exception + public ModuleTypeLoaderNotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes the exception with a particular module, error message and inner exception that happened. + /// + /// 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, + /// 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/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs new file mode 100644 index 0000000000..9fc6a10a72 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs @@ -0,0 +1,22 @@ +using System.Configuration; + +namespace Prism.Modularity +{ + /// + /// A for module configuration. + /// + public class ModulesConfigurationSection : ConfigurationSection + { + /// + /// Gets or sets the collection of modules configuration. + /// + /// A of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] + [ConfigurationProperty("", IsDefaultCollection = true, IsKey = false)] + public ModuleConfigurationElementCollection Modules + { + get { return (ModuleConfigurationElementCollection)base[""]; } + set { base[""] = value; } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs b/src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs new file mode 100644 index 0000000000..f66ca007de --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs @@ -0,0 +1,121 @@ +// TODO: This feature is currently disabled temporally +// NOTE: This is only used by Prism.WPF and not Prism.UNO, Prism.Forms, or Prism.MAUI +/* +using System; +using System.IO; +using Avalonia.Markup.Xaml; + +namespace Prism.Modularity +{ + /// + /// A catalog built from a XAML file. + /// + public class XamlModuleCatalog : ModuleCatalog + { + private readonly Uri _resourceUri; + + private const string _refFilePrefix = "file://"; + private int _refFilePrefixLength = _refFilePrefix.Length; + + /// + /// Creates an instance of a XamlResourceCatalog. + /// + /// The name of the XAML file + public XamlModuleCatalog(string fileName) + : this(new Uri(fileName, UriKind.Relative)) + { + } + + /// + /// Creates an instance of a XamlResourceCatalog. + /// + /// The pack url of the XAML file resource + public XamlModuleCatalog(Uri resourceUri) + { + _resourceUri = resourceUri; + } + + /// + /// Loads the catalog from the XAML file. + /// + protected override void InnerLoad() + { + var catalog = CreateFromXaml(_resourceUri); + + foreach (IModuleCatalogItem item in catalog.Items) + { + if (item is ModuleInfo mi) + { + if (!string.IsNullOrWhiteSpace(mi.Ref)) + mi.Ref = GetFileAbsoluteUri(mi.Ref); + } + else if (item is ModuleInfoGroup mg) + { + if (!string.IsNullOrWhiteSpace(mg.Ref)) + { + mg.Ref = GetFileAbsoluteUri(mg.Ref); + mg.UpdateModulesRef(); + } + else + { + foreach (var module in mg) + { + module.Ref = GetFileAbsoluteUri(module.Ref); + } + } + } + + Items.Add(item); + } + } + + /// + protected override string GetFileAbsoluteUri(string path) + { + //this is to maintain backwards compatibility with the old file:/// and file:// syntax for Xaml module catalog Ref property + if (path.StartsWith(_refFilePrefix + "/", StringComparison.Ordinal)) + { + path = path.Substring(_refFilePrefixLength + 1); + } + else if (path.StartsWith(_refFilePrefix, StringComparison.Ordinal)) + { + path = path.Substring(_refFilePrefixLength); + } + + return base.GetFileAbsoluteUri(path); + } + + /// + /// Creates a from XAML. + /// + /// that contains the XAML declaration of the catalog. + /// An instance of built from the XAML. + private static ModuleCatalog CreateFromXaml(Stream xamlStream) + { + if (xamlStream == null) + { + throw new ArgumentNullException(nameof(xamlStream)); + } + + return AvaloniaRuntimeXamlLoader.Load(xamlStream, null) as ModuleCatalog; + } + + /// + /// Creates a from a XAML included as an Application Resource. + /// + /// Relative that identifies the XAML included as an Application Resource. + /// An instance of build from the XAML. + private static ModuleCatalog CreateFromXaml(Uri builderResourceUri) + { + var streamInfo = System.Windows.Application.GetResourceStream(builderResourceUri); + + if ((streamInfo != null) && (streamInfo.Stream != null)) + { + return CreateFromXaml(streamInfo.Stream); + } + + return null; + } + } +} +*/ diff --git a/src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs b/src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs new file mode 100644 index 0000000000..b6c68f5d87 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs @@ -0,0 +1,69 @@ +using System.ComponentModel; +using System.Threading; +using System; +using Avalonia; +using Avalonia.Controls; + +namespace Prism.Mvvm +{ + /// + /// This class defines the attached property and related change handler that calls the ViewModelLocator in Prism.Mvvm. + /// + public static class ViewModelLocator + { + 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); + + /// + /// Gets the value for the attached property. + /// + /// The target element. + /// The attached to the element. + public static bool? GetAutoWireViewModel(AvaloniaObject obj) + { + return (bool?)obj.GetValue(AutoWireViewModelProperty); + } + + /// + /// Sets the attached property. + /// + /// The target element. + /// The value to attach. + public static void SetAutoWireViewModel(AvaloniaObject obj, bool value) + { + obj.SetValue(AutoWireViewModelProperty, value); + } + + private static void AutoWireViewModelChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + var value = (bool?)e.NewValue; + if (value.HasValue && value.Value) + { + ViewModelLocationProvider.AutoWireViewModelChanged(d, Bind); + } + } + + /// + /// Sets the DataContext of a View + /// + /// The View to set the DataContext on + /// The object to use as the DataContext for the View + static void Bind(object view, object viewModel) + { + if (view is Avalonia.Controls.Control element) + element.DataContext = viewModel; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs new file mode 100644 index 0000000000..4f43a6ade1 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs @@ -0,0 +1,27 @@ +using System; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// + /// Region that keeps all the views in it as active. Deactivation of views is not allowed. + /// + public class AllActiveRegion : Region + { + /// + /// Gets a readonly view of the collection of all the active views in the region. These are all the added views. + /// + /// An of all the active views. + public override IViewsCollection ActiveViews => Views; + + /// + /// Deactivate is not valid in this Region. This method will always throw . + /// + /// The view to deactivate. + /// Every time this method is called. + public override void Deactivate(object view) + { + throw new InvalidOperationException(Resources.DeactiveNotPossibleException); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs new file mode 100644 index 0000000000..52657c0626 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Prism.Ioc; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Populates the target region with the views registered to it in the . + /// + public class AutoPopulateRegionBehavior : RegionBehavior + { + /// + /// The key of this behavior. + /// + public const string BehaviorKey = "AutoPopulate"; + + private readonly IRegionViewRegistry regionViewRegistry; + + /// + /// Creates a new instance of the AutoPopulateRegionBehavior + /// associated with the received. + /// + /// that the behavior will monitor for views to populate the region. + public AutoPopulateRegionBehavior(IRegionViewRegistry regionViewRegistry) + { + this.regionViewRegistry = regionViewRegistry; + } + + /// + /// Attaches the AutoPopulateRegionBehavior to the Region. + /// + protected override void OnAttach() + { + if (string.IsNullOrEmpty(Region.Name)) + { + Region.PropertyChanged += Region_PropertyChanged; + } + else + { + StartPopulatingContent(); + } + } + + private void StartPopulatingContent() + { + foreach (object view in CreateViewsToAutoPopulate()) + { + AddViewIntoRegion(view); + } + + regionViewRegistry.ContentRegistered += OnViewRegistered; + } + + /// + /// Returns a collection of views that will be added to the + /// View collection. + /// + /// + protected virtual IEnumerable CreateViewsToAutoPopulate() + { + return regionViewRegistry.GetContents(Region.Name); + } + + /// + /// Adds a view into the views collection of this region. + /// + /// + protected virtual void AddViewIntoRegion(object viewToAdd) + { + Region.Add(viewToAdd); + } + + private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name)) + { + Region.PropertyChanged -= Region_PropertyChanged; + StartPopulatingContent(); + } + } + + /// + /// Handler of the event that fires when a new viewtype is registered to the registry. + /// + /// Although this is a public method to support Weak Delegates in Silverlight, it should not be called by the user. + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers", Justification = "This has to be public in order to work with weak references in partial trust or Silverlight environments.")] + public virtual void OnViewRegistered(object sender, ViewRegisteredEventArgs e) + { + if (e == null) + throw new ArgumentNullException(nameof(e)); + + if (e.RegionName == Region.Name) + { + AddViewIntoRegion(e.GetView(ContainerLocator.Container)); + } + } + } +} 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..27da651e5e --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs @@ -0,0 +1,102 @@ +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. + /// TODO (DS 2024-04-11): 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/ClearChildViewsRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs new file mode 100644 index 0000000000..94ffbd858b --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs @@ -0,0 +1,89 @@ +using Avalonia; +using Avalonia.Controls; +using Prism.Navigation.Regions; +using System; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Behavior that removes the RegionManager attached property of all the views in a region once the RegionManager property of a region becomes null. + /// This is useful when removing views with nested regions, to ensure these nested regions get removed from the RegionManager as well. + /// + /// This behavior does not apply by default. + /// In order to activate it, the ClearChildViews attached property must be set to True in the view containing the affected child regions. + /// + /// + public class ClearChildViewsRegionBehavior : RegionBehavior + { + /// + /// The behavior key. + /// + public const string BehaviorKey = "ClearChildViews"; + + /// + /// 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. + /// + public static readonly AvaloniaProperty ClearChildViewsProperty = + AvaloniaProperty.RegisterAttached("ClearChildViews", typeof(ClearChildViewsRegionBehavior)); + + /// + /// Gets the ClearChildViews attached property from a . + /// + /// The object from which to get the value. + /// The value of the ClearChildViews attached property in the target specified. + public static bool GetClearChildViews(AvaloniaObject target) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return (bool)target.GetValue(ClearChildViewsRegionBehavior.ClearChildViewsProperty); + } + + /// + /// Sets the ClearChildViews attached property in a . + /// + /// The object in which to set the value. + /// The value of to set in the target object's ClearChildViews attached property. + public static void SetClearChildViews(AvaloniaObject target, bool value) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + target.SetValue(ClearChildViewsRegionBehavior.ClearChildViewsProperty, value); + } + + /// + /// Subscribes to the 's PropertyChanged method to monitor its property. + /// + protected override void OnAttach() + { + Region.PropertyChanged += Region_PropertyChanged; + } + + private static void ClearChildViews(IRegion region) + { + foreach (var view in region.Views) + { + AvaloniaObject avaloniaObject = view as AvaloniaObject; + if (avaloniaObject != null) + { + if (GetClearChildViews(avaloniaObject)) + { + avaloniaObject.ClearValue(RegionManager.RegionManagerProperty); + } + } + } + } + + private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == "RegionManager") + { + if (Region.RegionManager == null) + { + ClearChildViews(Region); + } + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs new file mode 100644 index 0000000000..b20e641d9a --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Threading; +using Prism.Properties; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Behavior that creates a new , when the control that will host the (see ) + /// is added to the VisualTree. This behavior will use the class to find the right type of adapter to create + /// the region. After the region is created, this behavior will detach. + /// + /// + /// Attached property value inheritance is not available in Silverlight, so the current approach walks up the visual tree when requesting a region from a region manager. + /// The is now responsible for walking up the Tree. + /// + public class DelayedRegionCreationBehavior + { + private readonly RegionAdapterMappings regionAdapterMappings; + private WeakReference elementWeakReference; + private bool regionCreated; + + private static ICollection _instanceTracker = new Collection(); + private object _trackerLock = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The region adapter mappings, that are used to find the correct adapter for + /// a given control type. The control type is determined by the value. + /// + public DelayedRegionCreationBehavior(RegionAdapterMappings regionAdapterMappings) + { + this.regionAdapterMappings = regionAdapterMappings; + RegionManagerAccessor = new DefaultRegionManagerAccessor(); + } + + /// + /// Sets a class that interfaces between the 's static properties/events and this behavior, + /// so this behavior can be tested in isolation. + /// + /// The region manager accessor. + public IRegionManagerAccessor RegionManagerAccessor { get; set; } + + /// + /// The element that will host the Region. + /// + /// The target element. + public AvaloniaObject TargetElement + { + get { return elementWeakReference != null ? elementWeakReference.Target as AvaloniaObject : null; } + set { elementWeakReference = new WeakReference(value); } + } + + /// + /// Start monitoring the and the to detect when the becomes + /// part of the Visual Tree. When that happens, the Region will be created and the behavior will . + /// + public void Attach() + { + RegionManagerAccessor.UpdatingRegions += OnUpdatingRegions; + WireUpTargetElement(); + } + + /// + /// Stop monitoring the and the , so that this behavior can be garbage collected. + /// + public void Detach() + { + RegionManagerAccessor.UpdatingRegions -= OnUpdatingRegions; + UnWireTargetElement(); + } + + /// + /// Called when the is updating it's collection. + /// + /// + /// This method has to be public, because it has to be callable using weak references in silverlight and other partial trust environments. + /// + /// The . + /// The instance containing the event data. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers", Justification = "This has to be public in order to work with weak references in partial trust or Silverlight environments.")] + public void OnUpdatingRegions(object sender, EventArgs e) + { + TryCreateRegion(); + } + + private void TryCreateRegion() + { + AvaloniaObject targetElement = TargetElement; + if (targetElement == null) + { + Detach(); + return; + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Detach(); + + if (!regionCreated) + { + string regionName = RegionManagerAccessor.GetRegionName(targetElement); + CreateRegion(targetElement, regionName); + regionCreated = true; + } + } + } + + /// + /// Method that will create the region, by calling the right . + /// + /// The target element that will host the . + /// Name of the region. + /// The created + protected virtual IRegion CreateRegion(AvaloniaObject targetElement, string regionName) + { + if (targetElement == null) + throw new ArgumentNullException(nameof(targetElement)); + + try + { + // Build the region + IRegionAdapter regionAdapter = regionAdapterMappings.GetMapping(targetElement.GetType()); + IRegion region = regionAdapter.Initialize(targetElement, regionName); + + return region; + } + catch (Exception ex) + { + throw new RegionCreationException(string.Format(CultureInfo.CurrentCulture, Resources.RegionCreationException, regionName, ex), ex); + } + } + + private void ElementLoaded(object sender, VisualTreeAttachmentEventArgs e) + { + UnWireTargetElement(); + TryCreateRegion(); + } + + private void WireUpTargetElement() + { + Control element = TargetElement as Control; + if (element != null) + { + element.AttachedToVisualTree += ElementLoaded; + return; + } + + // TODO: NEEDS UPGRADED TO AVALONIA! + ////System.Windows.FrameworkContentElement fcElement = this.TargetElement as System.Windows.FrameworkContentElement; + ////Avalonia.Controls.Control fcElement = this.TargetElement as Control; + ////if (fcElement != null) + ////{ + //// fcElement.Loaded += this.ElementLoaded; + //// return; + ////} + + //if the element is a dependency object, and not a 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. + AvaloniaObject depObj = TargetElement as AvaloniaObject; + if (depObj != null) + { + Track(); + return; + } + } + + private void UnWireTargetElement() + { + Control element = TargetElement as Control; + if (element != null) + { + element.AttachedToVisualTree -= ElementLoaded; + return; + } + + // TODO: NEEDS UPGRADED TO AVALONIA! + //FrameworkContentElement fcElement = this.TargetElement as FrameworkContentElement; + //Avalonia.Controls.Control fcElement = this.TargetElement as Control; + //if (fcElement != null) + //{ + // fcElement.Loaded -= this.ElementLoaded; + // return; + //} + + AvaloniaObject depObj = TargetElement as AvaloniaObject; + if (depObj != null) + { + Untrack(); + return; + } + } + + /// + /// Add the instance of this class to to keep it alive + /// + private void Track() + { + lock (_trackerLock) + { + if (!_instanceTracker.Contains(this)) + { + _instanceTracker.Add(this); + } + } + } + + /// + /// Remove the instance of this class from + /// so it can eventually be garbage collected + /// + private void Untrack() + { + lock (_trackerLock) + { + _instanceTracker.Remove(this); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs new file mode 100644 index 0000000000..47252c3610 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Specialized; +using Prism.Common; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Calls on Views and ViewModels + /// removed from the collection. + /// + /// + /// The View and/or ViewModels must implement for this behavior to work. + /// + public class DestructibleRegionBehavior : RegionBehavior + { + /// + /// The key of this behavior. + /// + public const string BehaviorKey = "IDestructibleRegionBehavior"; + + /// + /// Attaches the to the collection. + /// + protected override void OnAttach() + { + Region.Views.CollectionChanged += Views_CollectionChanged; + } + + private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (var item in e.OldItems) + { + Action invocation = destructible => destructible.Destroy(); + MvvmHelpers.ViewAndViewModelAction(item, invocation); + } + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs new file mode 100644 index 0000000000..3bfb34a16c --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs @@ -0,0 +1,18 @@ +using Avalonia; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Defines a that not allows extensible behaviors on regions which also interact + /// with the target element that the is attached to. + /// + public interface IHostAwareRegionBehavior : IRegionBehavior + { + /// + /// Gets or sets the that the is attached to. + /// + /// A that the is attached to. + /// This is usually a that is part of the tree. + AvaloniaObject HostControl { get; set; } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs new file mode 100644 index 0000000000..7eb8bc2d13 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Prism.Common; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Behavior that monitors a object and + /// changes the value for the property when + /// an object that implements gets added or removed + /// from the collection. + /// + /// + /// This class can also sync the active state for any scoped regions directly on the view based on the . + /// If you use the method with the createRegionManagerScope option, the scoped manager will be attached to the view. + /// + public class RegionActiveAwareBehavior : IRegionBehavior + { + /// + /// Name that identifies the behavior in a collection of . + /// + public const string BehaviorKey = "ActiveAware"; + + /// + /// The region that this behavior is extending + /// + public IRegion Region { get; set; } + + /// + /// Attaches the behavior to the specified region + /// + public void Attach() + { + INotifyCollectionChanged collection = GetCollection(); + if (collection != null) + { + collection.CollectionChanged += OnCollectionChanged; + } + } + + /// + /// Detaches the behavior from the . + /// + public void Detach() + { + INotifyCollectionChanged collection = GetCollection(); + if (collection != null) + { + collection.CollectionChanged -= OnCollectionChanged; + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (object item in e.NewItems) + { + Action invocation = activeAware => activeAware.IsActive = true; + + MvvmHelpers.ViewAndViewModelAction(item, invocation); + InvokeOnSynchronizedActiveAwareChildren(item, invocation); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (object item in e.OldItems) + { + Action invocation = activeAware => activeAware.IsActive = false; + + MvvmHelpers.ViewAndViewModelAction(item, invocation); + InvokeOnSynchronizedActiveAwareChildren(item, invocation); + } + } + + // May need to handle other action values (reset, replace). Currently the ViewsCollection class does not raise CollectionChanged with these values. + } + + private void InvokeOnSynchronizedActiveAwareChildren(object item, Action invocation) + { + var avaloniaObjectView = item as AvaloniaObject; + + if (avaloniaObjectView != null) + { + // We are assuming that any scoped region managers are attached directly to the + // view. + var regionManager = RegionManager.GetRegionManager(avaloniaObjectView); + + // If the view's RegionManager attached property is different from the region's RegionManager, + // then the view's region manager is a scoped region manager. + if (regionManager == null || regionManager == Region.RegionManager) return; + + var activeViews = regionManager.Regions.SelectMany(e => e.ActiveViews); + + var syncActiveViews = activeViews.Where(ShouldSyncActiveState); + + foreach (var syncActiveView in syncActiveViews) + { + MvvmHelpers.ViewAndViewModelAction(syncActiveView, invocation); + } + } + } + + private bool ShouldSyncActiveState(object view) + { + if (Attribute.IsDefined(view.GetType(), typeof(SyncActiveStateAttribute))) + { + return true; + } + + var viewAsFrameworkElement = view as Control; + + if (viewAsFrameworkElement != null) + { + var viewModel = viewAsFrameworkElement.DataContext; + + return viewModel != null && Attribute.IsDefined(viewModel.GetType(), typeof(SyncActiveStateAttribute)); + } + + return false; + } + + private INotifyCollectionChanged GetCollection() + { + return Region.ActiveViews; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs new file mode 100644 index 0000000000..da9a3c322a --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs @@ -0,0 +1,158 @@ +using System; +using System.ComponentModel; +using Prism.Properties; +using Avalonia; +using Avalonia.Controls; +using Avalonia.VisualTree; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Subscribes to a static event from the in order to register the target + /// in a when one is available on the host control by walking up the tree and finding + /// a control whose property is not . + /// + public class RegionManagerRegistrationBehavior : RegionBehavior, IHostAwareRegionBehavior + { + /// + /// The key of this behavior. + /// + public static readonly string BehaviorKey = "RegionManagerRegistration"; + + private WeakReference attachedRegionManagerWeakReference; + private AvaloniaObject hostControl; + + /// + /// Initializes a new instance of . + /// + public RegionManagerRegistrationBehavior() + { + RegionManagerAccessor = new DefaultRegionManagerAccessor(); + } + + /// + /// Provides an abstraction on top of the RegionManager static members. + /// + public IRegionManagerAccessor RegionManagerAccessor { get; set; } + + /// + /// Gets or sets the that the is attached to. + /// + /// A that the is attached to. + /// This is usually a that is part of the tree. + /// When this member is set after the method has being called. + public AvaloniaObject HostControl + { + get + { + return hostControl; + } + set + { + if (IsAttached) + { + throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach); + } + + hostControl = value; + } + } + + /// + /// When the has a name assigned, the behavior will start monitoring the ancestor controls in the element tree + /// to look for an where to register the region in. + /// + protected override void OnAttach() + { + if (string.IsNullOrEmpty(Region.Name)) + { + Region.PropertyChanged += Region_PropertyChanged; + } + else + { + StartMonitoringRegionManager(); + } + } + + private void Region_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name)) + { + Region.PropertyChanged -= Region_PropertyChanged; + StartMonitoringRegionManager(); + } + } + + private void StartMonitoringRegionManager() + { + RegionManagerAccessor.UpdatingRegions += OnUpdatingRegions; + TryRegisterRegion(); + } + + private void TryRegisterRegion() + { + AvaloniaObject targetElement = HostControl; + if (targetElement.CheckAccess()) + { + IRegionManager regionManager = FindRegionManager(targetElement); + + IRegionManager attachedRegionManager = GetAttachedRegionManager(); + + if (regionManager != attachedRegionManager) + { + if (attachedRegionManager != null) + { + attachedRegionManagerWeakReference = null; + attachedRegionManager.Regions.Remove(Region.Name); + } + + if (regionManager != null) + { + attachedRegionManagerWeakReference = new WeakReference(regionManager); + regionManager.Regions.Add(Region); + } + } + } + } + + /// + /// This event handler gets called when a RegionManager is requering the instances of a region to be registered if they are not already. + /// Although this is a public method to support Weak Delegates in Silverlight, it should not be called by the user. + /// + /// The sender. + /// The arguments. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers", Justification = "This has to be public in order to work with weak references in partial trust or Silverlight environments.")] + public void OnUpdatingRegions(object sender, EventArgs e) + { + TryRegisterRegion(); + } + + private IRegionManager FindRegionManager(AvaloniaObject avaloniaObject) + { + var regionmanager = RegionManagerAccessor.GetRegionManager(avaloniaObject); + if (regionmanager != null) + { + return regionmanager; + } + + //TODO: this is should be ok in Avalonia. I have to test it + AvaloniaObject parent = ((avaloniaObject as Visual)?.GetVisualParent() ?? null) as AvaloniaObject; + if (parent != null) + { + return FindRegionManager(parent); + } + + return null; + } + + private IRegionManager GetAttachedRegionManager() + { + if (attachedRegionManagerWeakReference != null) + { + return attachedRegionManagerWeakReference.Target as IRegionManager; + } + + return null; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs new file mode 100644 index 0000000000..a09a6b4d02 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Controls; +using Prism.Common; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// The RegionMemberLifetimeBehavior determines if items should be removed from the + /// when they are deactivated. + /// + /// + /// The monitors the + /// collection to discover items that transition into a deactivated state. + ///

+ /// The behavior checks the removed items for either the + /// or the (in that order) to determine if it should be kept + /// alive on removal. + ///

+ /// If the item in the collection is a , it will + /// also check it's DataContext for or the . + ///

+ /// The order of checks are: + /// + /// Region Item's IRegionMemberLifetime.KeepAlive value. + /// Region Item's DataContext's IRegionMemberLifetime.KeepAlive value. + /// Region Item's RegionMemberLifetimeAttribute.KeepAlive value. + /// Region Item's DataContext's RegionMemberLifetimeAttribute.KeepAlive value. + /// + /// + public class RegionMemberLifetimeBehavior : RegionBehavior + { + ///

+ /// The key for this behavior. + /// + public const string BehaviorKey = "RegionMemberLifetimeBehavior"; + + /// + /// Override this method to perform the logic after the behavior has been attached. + /// + protected override void OnAttach() + { + Region.ActiveViews.CollectionChanged += OnActiveViewsChanged; + } + + private void OnActiveViewsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // We only pay attention to items removed from the ActiveViews list. + // Thus, we expect that any ICollectionView implementation would + // always raise a remove and we don't handle any resets + // unless we wanted to start tracking views that used to be active. + if (e.Action != NotifyCollectionChangedAction.Remove) return; + + var inactiveViews = e.OldItems; + foreach (var inactiveView in inactiveViews) + { + if (!ShouldKeepAlive(inactiveView)) + { + if (Region.Views.Contains(inactiveView)) + Region.Remove(inactiveView); + } + } + } + + private static bool ShouldKeepAlive(object inactiveView) + { + IRegionMemberLifetime lifetime = MvvmHelpers.GetImplementerFromViewOrViewModel(inactiveView); + if (lifetime != null) + { + return lifetime.KeepAlive; + } + + RegionMemberLifetimeAttribute lifetimeAttribute = GetItemOrContextLifetimeAttribute(inactiveView); + if (lifetimeAttribute != null) + { + return lifetimeAttribute.KeepAlive; + } + + return true; + } + + private static RegionMemberLifetimeAttribute GetItemOrContextLifetimeAttribute(object inactiveView) + { + var lifetimeAttribute = GetCustomAttributes(inactiveView.GetType()).FirstOrDefault(); + if (lifetimeAttribute != null) + { + return lifetimeAttribute; + } + + var control = inactiveView as Control; + if (control != null && control.DataContext != null) + { + var dataContext = control.DataContext; + var contextLifetimeAttribute = + GetCustomAttributes(dataContext.GetType()).FirstOrDefault(); + return contextLifetimeAttribute; + } + + return null; + } + + private static IEnumerable GetCustomAttributes(Type type) + { + return type.GetCustomAttributes(typeof(T), true).OfType(); + } + } +} 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..11cc9fe180 --- /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/Behaviors/SyncRegionContextWithHostBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs new file mode 100644 index 0000000000..93385e8d8d --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs @@ -0,0 +1,110 @@ +using System; +using Avalonia; +using Prism.Common; +using Prism.Properties; + +namespace Prism.Navigation.Regions.Behaviors +{ + /// + /// Behavior that synchronizes the property of a with + /// 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 data binding of the RegionContext from XAML. + /// + public class SyncRegionContextWithHostBehavior : RegionBehavior, IHostAwareRegionBehavior + { + private const string RegionContextPropertyName = "Context"; + private AvaloniaObject hostControl; + + /// + /// Name that identifies the SyncRegionContextWithHostBehavior behavior in a collection of RegionsBehaviors. + /// + public static readonly string BehaviorKey = "SyncRegionContextWithHost"; + + private ObservableObject HostControlRegionContext + { + get + { + return RegionContext.GetObservableContext(hostControl); + } + } + + /// + /// Gets or sets the that the is attached to. + /// + /// + /// A that the is attached to. + /// This is usually a that is part of the tree. + /// + public AvaloniaObject HostControl + { + get + { + return hostControl; + } + set + { + if (IsAttached) + { + throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach); + } + + hostControl = value; + } + } + + /// + /// Override this method to perform the logic after the behavior has been attached. + /// + protected override void OnAttach() + { + if (HostControl != null) + { + // Sync values initially. + SynchronizeRegionContext(); + + // Now register for events to keep them in sync + HostControlRegionContext.PropertyChanged += RegionContextObservableObject_PropertyChanged; + Region.PropertyChanged += Region_PropertyChanged; + } + } + + private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == RegionContextPropertyName) + { + if (RegionManager.GetRegionContext(HostControl) != Region.Context) + { + // Setting this Dependency Property will automatically also change the HostControlRegionContext.Value + // (see RegionManager.OnRegionContextChanged()) + RegionManager.SetRegionContext(hostControl, Region.Context); + } + } + } + + private void RegionContextObservableObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == "Value") + { + SynchronizeRegionContext(); + } + } + + private void SynchronizeRegionContext() + { + // Forward this value to the Region + if (Region.Context != HostControlRegionContext.Value) + { + Region.Context = HostControlRegionContext.Value; + } + + // Also make sure the region's StyledProperty was changed (this can occur if the value + // was changed only on the HostControlRegionContext) + if (RegionManager.GetRegionContext(HostControl) != HostControlRegionContext.Value) + { + RegionManager.SetRegionContext(HostControl, HostControlRegionContext.Value); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs new file mode 100644 index 0000000000..f2a10cf66b --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs @@ -0,0 +1,64 @@ +using Avalonia.Controls; +using Prism.Properties; +using System; +using System.Collections.Specialized; +using System.Linq; + +namespace Prism.Navigation.Regions +{ + /// + /// Adapter that creates a new and monitors its + /// active view to set it on the adapted . + /// + public class ContentControlRegionAdapter : RegionAdapterBase + { + /// + /// Initializes a new instance of . + /// + /// The factory used to create the region behaviors to attach to the created regions. + public ContentControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) + : base(regionBehaviorFactory) + { + } + + /// + /// Adapts a to an . + /// + /// The new region being used. + /// The object to adapt. + protected override void Adapt(IRegion region, ContentControl regionTarget) + { + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + bool contentIsSet = regionTarget.Content != null; + contentIsSet = contentIsSet || regionTarget[ContentControl.ContentProperty] != null; + + if (contentIsSet) + throw new InvalidOperationException(Resources.ContentControlHasContentException); + + region.ActiveViews.CollectionChanged += delegate + { + regionTarget.Content = region.ActiveViews.FirstOrDefault(); + }; + + region.Views.CollectionChanged += + (sender, e) => + { + if (e.Action == NotifyCollectionChangedAction.Add && region.ActiveViews.Count() == 0) + { + region.Activate(e.NewItems[0]); + } + }; + } + + /// + /// Creates a new instance of . + /// + /// A new instance of . + protected override IRegion CreateRegion() + { + return new SingleActiveRegion(); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs new file mode 100644 index 0000000000..2f4d1c49cd --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs @@ -0,0 +1,46 @@ +using System; +using Avalonia; + +namespace Prism.Navigation.Regions +{ + internal class DefaultRegionManagerAccessor : IRegionManagerAccessor + { + /// + /// Notification used by attached behaviors to update the region managers appropriatelly if needed to. + /// + /// This event uses weak references to the event handler to prevent this static event of keeping the + /// target element longer than expected. + public event EventHandler UpdatingRegions + { + add { RegionManager.UpdatingRegions += value; } + remove { RegionManager.UpdatingRegions -= value; } + } + + /// + /// Gets the value for the RegionName attached property. + /// + /// 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. + public string GetRegionName(AvaloniaObject element) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); + + return element.GetValue(RegionManager.RegionNameProperty) as string; + } + + /// + /// Gets the value of the RegionName attached property. + /// + /// The target element. + /// The attached to the element. + public IRegionManager GetRegionManager(AvaloniaObject element) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); + + return element.GetValue(RegionManager.RegionManagerProperty) as IRegionManager; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs new file mode 100644 index 0000000000..94e2eb301e --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs @@ -0,0 +1,8 @@ +namespace Prism.Navigation.Regions +{ + /// Provides a way for objects involved in navigation to be notified of navigation activities. + /// Provides compatibility for Legacy Prism.Avalonia apps. + public interface INavigationAware : IRegionAware + { + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs new file mode 100644 index 0000000000..850e697b16 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia; + +namespace Prism.Navigation.Regions +{ + /// + /// Provides an abstraction on top of the RegionManager static members. + /// + public interface IRegionManagerAccessor + { + /// + /// Notification used by attached behaviors to update the region managers appropriately if needed to. + /// + /// This event uses weak references to the event handler to prevent this static event of keeping the + /// target element longer than expected. + event EventHandler UpdatingRegions; + + /// + /// Gets the value for the RegionName attached property. + /// + /// 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. + string GetRegionName(AvaloniaObject element); + + /// + /// Gets the value of the RegionName attached property. + /// + /// The target element. + /// The attached to the element. + IRegionManager GetRegionManager(AvaloniaObject element); + } +} 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..85e32d6b5a --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia; + +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 { return 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 { return 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/Navigation/Regions/ItemsControlRegionAdapter.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemsControlRegionAdapter.cs new file mode 100644 index 0000000000..f3859d7464 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemsControlRegionAdapter.cs @@ -0,0 +1,71 @@ +using System; +using Avalonia.Controls; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// + /// Adapter that creates a new and binds all + /// the views to the adapted . + /// + public class ItemsControlRegionAdapter : RegionAdapterBase + { + /// + /// Initializes a new instance of . + /// + /// The factory used to create the region behaviors to attach to the created regions. + public ItemsControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) + : base(regionBehaviorFactory) + { + } + + /// + /// Adapts an to an . + /// + /// The new region being used. + /// The object to adapt. + protected override void Adapt(IRegion region, ItemsControl regionTarget) + { + if (region == null) + throw new ArgumentNullException(nameof(region)); + + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + // NOTE: In Avalonia, Items will never be null + bool itemsSourceIsSet = regionTarget.ItemCount > 0; + itemsSourceIsSet = itemsSourceIsSet || regionTarget.HasBinding(ItemsControl.ItemsSourceProperty); + + if (itemsSourceIsSet) + { + throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException); + } + + // 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(); + } + + // Avalonia v11-Preview5 needs IRegion implement IList. Enforcing it to return AvaloniaList fixes this. + // Avalonia v11-Preview8 ItemsControl.Items is readonly (#10827). + ////regionTarget.Items = region.Views as Avalonia.Collections.AvaloniaList; + regionTarget.ItemsSource = region.Views as Avalonia.Collections.AvaloniaList; + } + + /// + /// Creates a new instance of . + /// + /// A new instance of . + protected override IRegion CreateRegion() + { + return new AllActiveRegion(); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs new file mode 100644 index 0000000000..f445c3fa74 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Avalonia; +using Prism.Ioc; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// Implementation of that allows multiple active views. + public class Region : IRegion + { + private ObservableCollection _itemMetadataCollection; + private string _name; + private ViewsCollection _views; + private ViewsCollection _activeViews; + private object _context; + private IRegionManager _regionManager; + private IRegionNavigationService _regionNavigationService; + + private Comparison _sort; + + /// + /// Initializes a new instance of . + /// + public Region() + { + Behaviors = new RegionBehaviorCollection(this); + + _sort = DefaultSortComparison; + } + + /// Occurs when a property value changes. + public event PropertyChangedEventHandler PropertyChanged; + + /// Gets the collection of s that can extend the behavior of regions. + public IRegionBehaviorCollection Behaviors { get; } + + /// Gets or sets a context for the region. This value can be used by the user to share context with the views. + /// The context value to be shared. + public object Context + { + get => _context; + + set + { + if (_context != value) + { + _context = value; + OnPropertyChanged(nameof(Context)); + } + } + } + + /// + /// Gets the name of the region that uniequely identifies the region within a . + /// + /// The name of the region. + public string Name + { + get => _name; + + set + { + if (_name != null && _name != value) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.CannotChangeRegionNameException, _name)); + } + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(Resources.RegionNameCannotBeEmptyException); + } + + _name = value; + OnPropertyChanged(nameof(Name)); + } + } + + /// + /// Gets a readonly view of the collection of views in the region. + /// + /// An of all the added views. + public virtual IViewsCollection Views + { + get + { + if (_views == null) + { + _views = new ViewsCollection(ItemMetadataCollection, x => true) + { + SortComparison = _sort + }; + } + + return _views; + } + } + + /// + /// Gets a readonly view of the collection of all the active views in the region. + /// + /// An of all the active views. + public virtual IViewsCollection ActiveViews + { + get + { + if (_views == null) + { + _views = new ViewsCollection(ItemMetadataCollection, x => true) + { + SortComparison = _sort + }; + } + + if (_activeViews == null) + { + _activeViews = new ViewsCollection(ItemMetadataCollection, x => x.IsActive) + { + SortComparison = _sort + }; + } + + return _activeViews; + } + } + + /// + /// Gets or sets the comparison used to sort the views. + /// + /// The comparison to use. + public Comparison SortComparison + { + get => _sort; + set + { + _sort = value; + + if (_activeViews != null) + { + _activeViews.SortComparison = _sort; + } + + if (_views != null) + { + _views.SortComparison = _sort; + } + } + } + + /// + /// Gets or sets the that will be passed to the views when adding them to the region, unless the view is added by specifying createRegionManagerScope as . + /// + /// The where this is registered. + /// This is usually used by implementations of and should not be + /// used by the developer explicitly. + public IRegionManager RegionManager + { + get => _regionManager; + + set + { + if (_regionManager != value) + { + _regionManager = value; + OnPropertyChanged(nameof(RegionManager)); + } + } + } + + /// + /// Gets the navigation service. + /// + /// The navigation service. + public IRegionNavigationService NavigationService + { + get + { + if (_regionNavigationService == null) + { + _regionNavigationService = ContainerLocator.Container.Resolve(); + _regionNavigationService.Region = this; + } + + return _regionNavigationService; + } + + set => _regionNavigationService = value; + } + + /// + /// Gets the collection with all the views along with their metadata. + /// + /// An of with all the added views. + protected virtual ObservableCollection ItemMetadataCollection + { + get + { + _itemMetadataCollection ??= new ObservableCollection(); + return _itemMetadataCollection; + } + } + + /// Adds a new view to the region. + /// Adds a new view to the region. + /// The view to add. + /// The that is set on the view if it is a . It will be the current region manager when using this overload. + public IRegionManager Add(string viewName) + { + var view = ContainerLocator.Container.Resolve(viewName); + return Add(view, viewName, false); + } + + /// Adds a new view to the region. + /// + /// Adds a new view to the region. + /// + /// The view to add. + /// The that is set on the view if it is a . It will be the current region manager when using this overload. + public IRegionManager Add(object view) + { + return Add(view, null, false); + } + + /// Adds a new view to the region. + /// The view to add. + /// The name of the view. This can be used to retrieve it later by calling . + /// The that is set on the view if it is a . It will be the current region manager when using this overload. + public IRegionManager Add(object view, string viewName) + { + if (string.IsNullOrEmpty(viewName)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeNullOrEmpty, "viewName")); + } + + return Add(view, viewName, false); + } + + /// Adds a new view to the region. + /// 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. + /// The that is set on the view if it is a . + public virtual IRegionManager Add(object view, string viewName, bool createRegionManagerScope) + { + IRegionManager manager = createRegionManagerScope ? RegionManager.CreateRegionManager() : RegionManager; + InnerAdd(view, viewName, manager); + return manager; + } + + /// Removes the specified view from the region. + /// The view to remove. + public virtual void Remove(object view) + { + ItemMetadata itemMetadata = GetItemMetadataOrThrow(view); + + ItemMetadataCollection.Remove(itemMetadata); + + if (view is AvaloniaObject avaloniaObject && Regions.RegionManager.GetRegionManager(avaloniaObject) == RegionManager) + { + avaloniaObject.ClearValue(Regions.RegionManager.RegionManagerProperty); + } + } + + /// Removes all views from the region. + public void RemoveAll() + { + foreach (var view in Views) + { + Remove(view); + } + } + + /// Marks the specified view as active. + /// The view to activate. + public virtual void Activate(object view) + { + ItemMetadata itemMetadata = GetItemMetadataOrThrow(view); + + if (!itemMetadata.IsActive) + { + itemMetadata.IsActive = true; + } + } + + /// Marks the specified view as inactive. + /// The view to deactivate. + public virtual void Deactivate(object view) + { + ItemMetadata itemMetadata = GetItemMetadataOrThrow(view); + + if (itemMetadata.IsActive) + { + itemMetadata.IsActive = false; + } + } + + /// Returns the view instance that was added to the region using a specific name. + /// The name used when adding the view to the region. + /// Returns the named view or if the view with does not exist in the current region. + public virtual object GetView(string viewName) + { + if (string.IsNullOrEmpty(viewName)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeNullOrEmpty, "viewName")); + } + + ItemMetadata metadata = ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName); + + if (metadata != null) + { + return metadata.Item; + } + + return null; + } + + /// Initiates navigation to the specified target. + /// The target. + /// A callback to execute when the navigation request is completed. + public void RequestNavigate(Uri target, Action navigationCallback) + { + RequestNavigate(target, navigationCallback, null); + } + + /// Initiates navigation to the specified target. + /// The target. + /// A callback to execute when the navigation request is completed. + /// The navigation parameters specific to the navigation request. + public void RequestNavigate(Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + NavigationService.RequestNavigate(target, navigationCallback, navigationParameters); + } + + private void InnerAdd(object view, string viewName, IRegionManager scopedRegionManager) + { + if (ItemMetadataCollection.FirstOrDefault(x => x.Item == view) != null) + { + throw new InvalidOperationException(Resources.RegionViewExistsException); + } + + var itemMetadata = new ItemMetadata(view); + if (!string.IsNullOrEmpty(viewName)) + { + if (ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName) != null) + { + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.RegionViewNameExistsException, viewName)); + } + + itemMetadata.Name = viewName; + } + + if (view is AvaloniaObject avaloniaObject) + { + Regions.RegionManager.SetRegionManager(avaloniaObject, scopedRegionManager); + } + + ItemMetadataCollection.Add(itemMetadata); + } + + private ItemMetadata GetItemMetadataOrThrow(object view) + { + if (view == null) + throw new ArgumentNullException(nameof(view)); + + ItemMetadata itemMetadata = ItemMetadataCollection.FirstOrDefault(x => x.Item == view); + + if (itemMetadata == null) + throw new ArgumentException(Resources.ViewNotInRegionException, nameof(view)); + + return itemMetadata; + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + + /// + /// The default sort algorithm. + /// + /// The first view to compare. + /// The second view to compare. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x")] + public static int DefaultSortComparison(object x, object y) + { + if (x == null) + { + if (y == null) + { + return 0; + } + else + { + return -1; + } + } + else + { + if (y == null) + { + return 1; + } + else + { + Type xType = x.GetType(); + Type yType = y.GetType(); + + ViewSortHintAttribute xAttribute = xType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute; + ViewSortHintAttribute yAttribute = yType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute; + + return ViewSortHintAttributeComparison(xAttribute, yAttribute); + } + } + } + + private static int ViewSortHintAttributeComparison(ViewSortHintAttribute x, ViewSortHintAttribute y) + { + if (x == null) + { + if (y == null) + { + return 0; + } + else + { + return -1; + } + } + else + { + if (y == null) + { + return 1; + } + else + { + return string.Compare(x.Hint, y.Hint, StringComparison.Ordinal); + } + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs new file mode 100644 index 0000000000..d84d3356d9 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs @@ -0,0 +1,154 @@ +using System; +using System.Globalization; +using Avalonia; +using Prism.Navigation.Regions.Behaviors; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// + /// Base class to facilitate the creation of implementations. + /// + /// Type of object to adapt. + public abstract class RegionAdapterBase : IRegionAdapter where T : class + { + /// + /// Initializes a new instance of . + /// + /// The factory used to create the region behaviors to attach to the created regions. + protected RegionAdapterBase(IRegionBehaviorFactory regionBehaviorFactory) + { + RegionBehaviorFactory = regionBehaviorFactory; + } + + /// + /// Gets or sets the factory used to create the region behaviors to attach to the created regions. + /// + protected IRegionBehaviorFactory RegionBehaviorFactory { get; set; } + + /// + /// Adapts an object and binds it to a new . + /// + /// The object to adapt. + /// The name of the region to be created. + /// The new instance of that the is bound to. + public IRegion Initialize(T regionTarget, string regionName) + { + if (regionName == null) + throw new ArgumentNullException(nameof(regionName)); + + IRegion region = CreateRegion(); + region.Name = regionName; + + SetObservableRegionOnHostingControl(region, regionTarget); + + Adapt(region, regionTarget); + AttachBehaviors(region, regionTarget); + AttachDefaultBehaviors(region, regionTarget); + return region; + } + + /// + /// Adapts an object and binds it to a new . + /// + /// The object to adapt. + /// The name of the region to be created. + /// The new instance of that the is bound to. + /// This methods performs validation to check that + /// is of type . + /// When is . + /// When is not of type . + IRegion IRegionAdapter.Initialize(object regionTarget, string regionName) + { + return Initialize(GetCastedObject(regionTarget), regionName); + } + + /// + /// This method adds the default behaviors by using the object. + /// + /// The region being used. + /// The object to adapt. + protected virtual void AttachDefaultBehaviors(IRegion region, T regionTarget) + { + if (region == null) + throw new ArgumentNullException(nameof(region)); + + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + IRegionBehaviorFactory behaviorFactory = RegionBehaviorFactory; + if (behaviorFactory != null) + { + AvaloniaObject avaloniaObjectRegionTarget = regionTarget as AvaloniaObject; + + foreach (string behaviorKey in behaviorFactory) + { + if (!region.Behaviors.ContainsKey(behaviorKey)) + { + IRegionBehavior behavior = behaviorFactory.CreateFromKey(behaviorKey); + + if (avaloniaObjectRegionTarget != null) + { + IHostAwareRegionBehavior hostAwareRegionBehavior = behavior as IHostAwareRegionBehavior; + if (hostAwareRegionBehavior != null) + { + hostAwareRegionBehavior.HostControl = avaloniaObjectRegionTarget; + } + } + + region.Behaviors.Add(behaviorKey, behavior); + } + } + } + } + + /// + /// Template method to attach new behaviors. + /// + /// The region being used. + /// The object to adapt. + protected virtual void AttachBehaviors(IRegion region, T regionTarget) + { + } + + /// + /// Template method to adapt the object to an . + /// + /// The new region being used. + /// The object to adapt. + protected abstract void Adapt(IRegion region, T regionTarget); + + /// + /// Template method to create a new instance of + /// that will be used to adapt the object. + /// + /// A new instance of . + protected abstract IRegion CreateRegion(); + + private static T GetCastedObject(object regionTarget) + { + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + T castedObject = regionTarget as T; + + if (castedObject == null) + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.AdapterInvalidTypeException, typeof(T).Name)); + + return castedObject; + } + + private static void SetObservableRegionOnHostingControl(IRegion region, T regionTarget) + { + AvaloniaObject targetElement = regionTarget as AvaloniaObject; + + if (targetElement != null) + { + // Set the region as a dependency property on the control hosting the region + // Because we are using an observable region, the hosting control can detect that the + // region has actually been created. This is an ideal moment to hook up custom behaviors + RegionManager.GetObservableRegion(targetElement).Value = region; + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs new file mode 100644 index 0000000000..fc818110f7 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Prism.Ioc; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// + /// This class maps with . + /// + public class RegionAdapterMappings + { + private readonly Dictionary mappings = new Dictionary(); + + /// + /// Registers the mapping between a type and an adapter. + /// + /// The type of the control. + /// The adapter to use with the type. + /// When any of or are . + /// If a mapping for already exists. + public void RegisterMapping(Type controlType, IRegionAdapter adapter) + { + if (controlType == null) + throw new ArgumentNullException(nameof(controlType)); + + if (adapter == null) + throw new ArgumentNullException(nameof(adapter)); + + if (mappings.ContainsKey(controlType)) + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + Resources.MappingExistsException, controlType.Name)); + + mappings.Add(controlType, adapter); + } + + /// + /// Registers the mapping between a type and an adapter. + /// + /// The type of the control + public void RegisterMapping(IRegionAdapter adapter) + { + RegisterMapping(typeof(TControl), adapter); + } + + /// + /// Registers the mapping between a type and an adapter. + /// + /// The type of the control + /// The type of the IRegionAdapter to use with the TControl + public void RegisterMapping() where TAdapter : IRegionAdapter + { + RegisterMapping(typeof(TControl), ContainerLocator.Container.Resolve()); + } + + /// + /// Returns the adapter associated with the type provided. + /// + /// The type to obtain the mapped. + /// The mapped to the . + /// This class will look for a registered type for and if there is not any, + /// it will look for a registered type for any of its ancestors in the class hierarchy. + /// If there is no registered type for or any of its ancestors, + /// an exception will be thrown. + /// When there is no registered type for or any of its ancestors. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "controlType")] + public IRegionAdapter GetMapping(Type controlType) + { + Type currentType = controlType; + + while (currentType != null) + { + if (mappings.ContainsKey(currentType)) + { + return mappings[currentType]; + } + + currentType = currentType.BaseType; + } + + throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, Resources.NoRegionAdapterException, controlType)); + } + + /// + /// Returns the adapter associated with the type provided. + /// + /// The control type used to obtain the mapped. + /// The mapped to the . + /// This class will look for a registered type for and if there is not any, + /// it will look for a registered type for any of its ancestors in the class hierarchy. + /// If there is no registered type for or any of its ancestors, + /// an exception will be thrown. + /// When there is no registered type for or any of its ancestors. + public IRegionAdapter GetMapping() + { + return GetMapping(typeof(T)); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs new file mode 100644 index 0000000000..29a129a36c --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs @@ -0,0 +1,50 @@ +using System; +using Avalonia; +using Prism.Common; + +namespace Prism.Navigation.Regions +{ + /// + /// 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 + { + private static readonly AvaloniaProperty ObservableRegionContextProperty = + AvaloniaProperty.RegisterAttached>("ObservableRegionContext", typeof(RegionContext)); + + static RegionContext() + { + ObservableRegionContextProperty.Changed.Subscribe(args => GetObservableContext(args?.Sender as Visual)); + } + + /// + /// 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 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. + public static ObservableObject GetObservableContext(AvaloniaObject view) + { + if (view == null) + throw new ArgumentNullException(nameof(view)); + + ObservableObject context = view.GetValue(ObservableRegionContextProperty) as ObservableObject; + + if (context == null) + { + context = new ObservableObject(); + view.SetValue(ObservableRegionContextProperty, context); + } + + return context; + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs new file mode 100644 index 0000000000..b12b2ed62a --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs @@ -0,0 +1,550 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using Prism.Common; +using Prism.Events; +using Prism.Ioc; +using Prism.Properties; +using Prism.Ioc.Internals; +using Avalonia; +using Avalonia.Controls; +using Prism.Navigation.Regions.Behaviors; + +namespace Prism.Navigation.Regions +{ + /// + /// This class is responsible for maintaining a collection of regions and attaching regions to controls. + /// + /// + /// This class supplies the attached properties that can be used for simple region creation from XAML. + /// + public class RegionManager : IRegionManager + { + #region Static members (for XAML support) + + private static readonly WeakDelegatesManager updatingRegionsListeners = new WeakDelegatesManager(); + + /// + /// Identifies the RegionName attached property. + /// + /// + /// When a control has both the and + /// attached properties set to + /// a value different than and there is a + /// mapping registered for the control, it + /// will create and adapt a new region for that control, and register it + /// in the with the specified region name. + /// + public static readonly AvaloniaProperty RegionNameProperty = AvaloniaProperty.RegisterAttached( + "RegionName", + typeof(RegionManager)); + + /// + /// Sets the attached property. + /// + /// The object to adapt. This is typically a container (i.e a control). + /// The name of the region to register. + public static void SetRegionName(AvaloniaObject regionTarget, string regionName) + { + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + regionTarget.SetValue(RegionNameProperty, regionName); + } + + /// + /// Gets the value for the attached property. + /// + /// The object to adapt. This is typically a container (i.e a control). + /// The name of the region that should be created when + /// is also set in this element. + public static string GetRegionName(AvaloniaObject regionTarget) + { + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + return regionTarget.GetValue(RegionNameProperty) as string; + } + + private static readonly AvaloniaProperty ObservableRegionProperty = + AvaloniaProperty.RegisterAttached>("ObservableRegion", typeof(RegionManager)); + + /// + /// Returns an wrapper that can hold an . Using this wrapper + /// you can detect when an has been created by the . + /// + /// If the wrapper does not yet exist, a new wrapper will be created. When the region + /// gets created and assigned to the wrapper, you can use the event + /// to get notified of that change. + /// + /// The view that will host the region. + /// Wrapper that can hold an value and can notify when the value changes. + public static ObservableObject GetObservableRegion(AvaloniaObject view) + { + if (view == null) throw new ArgumentNullException(nameof(view)); + + ObservableObject regionWrapper = view.GetValue(ObservableRegionProperty) as ObservableObject; + + if (regionWrapper == null) + { + regionWrapper = new ObservableObject(); + view.SetValue(ObservableRegionProperty, regionWrapper); + } + + return regionWrapper; + } + + private static void OnSetRegionNameCallback(AvaloniaObject element, AvaloniaPropertyChangedEventArgs args) + { + if (!IsInDesignMode(element)) + { + CreateRegion(element); + } + } + + private static void CreateRegion(AvaloniaObject element) + { + var container = ContainerLocator.Container; + DelayedRegionCreationBehavior regionCreationBehavior = container.Resolve(); + regionCreationBehavior.TargetElement = element; + regionCreationBehavior.Attach(); + } + + /// + /// Identifies the RegionManager attached property. + /// + /// + /// When a control has both the and + /// attached properties set to + /// a value different than and there is a + /// mapping registered for the control, it + /// will create and adapt a new region for that control, and register it + /// in the with the specified region name. + /// + public static readonly AvaloniaProperty RegionManagerProperty = + AvaloniaProperty.RegisterAttached("RegionManager", typeof(RegionManager)); + + /// + /// Gets the value of the attached property. + /// + /// The target element. + /// The attached to the element. + public static IRegionManager GetRegionManager(AvaloniaObject target) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return (IRegionManager)target.GetValue(RegionManagerProperty); + } + + /// + /// Sets the attached property. + /// + /// The target element. + /// The value. + public static void SetRegionManager(AvaloniaObject target, IRegionManager value) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + target.SetValue(RegionManagerProperty, value); + } + + /// + /// Identifies the RegionContext attached property. + /// + public static readonly AvaloniaProperty RegionContextProperty = + AvaloniaProperty.RegisterAttached("RegionContext", typeof(RegionManager)); + + private static void OnRegionContextChanged(AvaloniaObject depObj, AvaloniaPropertyChangedEventArgs e) + { + if (RegionContext.GetObservableContext(depObj as AvaloniaObject).Value != e.NewValue) + { + RegionContext.GetObservableContext(depObj as AvaloniaObject).Value = e.NewValue; + } + } + + /// + /// Gets the value of the attached property. + /// + /// The target element. + /// The region context to pass to the contained views. + public static object GetRegionContext(AvaloniaObject target) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return target.GetValue(RegionContextProperty); + } + + /// + /// Sets the attached property. + /// + /// The target element. + /// The value. + public static void SetRegionContext(AvaloniaObject target, object value) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + target.SetValue(RegionContextProperty, value); + } + + /// + /// Notification used by attached behaviors to update the region managers appropriately if needed to. + /// + /// This event uses weak references to the event handler to prevent this static event of keeping the + /// target element longer than expected. + public static event EventHandler UpdatingRegions + { + add { updatingRegionsListeners.AddListener(value); } + remove { updatingRegionsListeners.RemoveListener(value); } + } + + /// + /// Notifies attached behaviors to update the region managers appropriately if needed to. + /// + /// + /// This method is normally called internally, and there is usually no need to call this from user code. + /// + public static void UpdateRegions() + { + + try + { + updatingRegionsListeners.Raise(null, EventArgs.Empty); + } + catch (TargetInvocationException ex) + { + Exception rootException = ex.GetRootException(); + + throw new UpdateRegionsException(string.Format(CultureInfo.CurrentCulture, + Resources.UpdateRegionException, rootException), ex.InnerException); + } + } + + private static bool IsInDesignMode(AvaloniaObject element) + { + return Design.IsDesignMode; + } + + #endregion + + private readonly RegionCollection regionCollection; + + /// + /// Initializes a new instance of . + /// + public RegionManager() + { + regionCollection = new RegionCollection(this); + } + + 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)); + } + + /// + /// 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. + /// + /// A with all the registered regions. + public IRegionCollection Regions + { + get { return regionCollection; } + } + + /// + /// Creates a new region manager. + /// + /// A new region manager that can be used as a different scope from the current region manager. + public IRegionManager CreateRegionManager() + { + return new RegionManager(); + } + + /// + /// Add a view to the Views collection of a Region. Note that the region must already exist in this . + /// + /// The name of the region to add a view to + /// The view to add to the views collection + /// The RegionManager, to easily add several views. + public IRegionManager AddToRegion(string regionName, object view) + { + if (!Regions.ContainsRegionWithName(regionName)) + throw new ArgumentException(string.Format(Thread.CurrentThread.CurrentCulture, Resources.RegionNotFound, regionName), nameof(regionName)); + + return Regions[regionName].Add(view); + } + + /// + /// Add a view to the Views collection of a Region. Note that the region must already exist in this . + /// + /// The name of the region to add a view to + /// The view to add to the views collection + /// The RegionManager, to easily add several views. + public IRegionManager AddToRegion(string regionName, string targetName) + { + if (!Regions.ContainsRegionWithName(regionName)) + throw new ArgumentException(string.Format(Thread.CurrentThread.CurrentCulture, Resources.RegionNotFound, regionName), nameof(regionName)); + + var view = CreateNewRegionItem(targetName); + + return Regions[regionName].Add(view); + } + + /// + /// Associate a view with a region, by registering a type. When the region get's displayed + /// this type will be resolved using the ServiceLocator into a concrete instance. The instance + /// will be added to the Views collection of the region + /// + /// The name of the region to associate the view with. + /// The type of the view to register with the + /// The , for adding several views easily + public IRegionManager RegisterViewWithRegion(string regionName, Type viewType) + { + var regionViewRegistry = ContainerLocator.Container.Resolve(); + + regionViewRegistry.RegisterViewWithRegion(regionName, viewType); + + return this; + } + + /// + /// Associate a view with a region, by registering a type. When the region get's displayed + /// this type will be resolved using the ServiceLocator into a concrete instance. The instance + /// will be added to the Views collection of the region + /// + /// The name of the region to associate the view with. + /// The type of the view to register with the + /// The , for adding several views easily + public IRegionManager RegisterViewWithRegion(string regionName, string targetName) + { + var viewType = ContainerLocator.Current.GetRegistrationType(targetName); + + return RegisterViewWithRegion(regionName, viewType); + } + + /// + /// Associate a view with a region, using a delegate to resolve a concrete instance of the view. + /// When the region get's displayed, this delegate will be called and the result will be added to the + /// views collection of the region. + /// + /// The name of the region to associate the view with. + /// The delegate used to resolve a concrete instance of the view. + /// The , for adding several views easily + public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + var regionViewRegistry = ContainerLocator.Container.Resolve(); + + regionViewRegistry.RegisterViewWithRegion(regionName, getContentDelegate); + + return this; + } + + /// + /// Navigates the specified region manager. + /// + /// The name of the region to call Navigate on. + /// The URI of the content to display. + /// The navigation callback. + public void RequestNavigate(string regionName, Uri source, Action navigationCallback) + { + if (navigationCallback == null) + throw new ArgumentNullException(nameof(navigationCallback)); + + if (Regions.ContainsRegionWithName(regionName)) + { + Regions[regionName].RequestNavigate(source, navigationCallback); + } + else + { + navigationCallback(new NavigationResult(new NavigationContext(null, source), false)); + } + } + + /// + /// 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 , which holds a collection of object parameters. + public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + if (navigationCallback == null) + throw new ArgumentNullException(nameof(navigationCallback)); + + if (Regions.ContainsRegionWithName(regionName)) + { + Regions[regionName].RequestNavigate(target, navigationCallback, navigationParameters); + } + else + { + navigationCallback(new NavigationResult(new NavigationContext(null, target, navigationParameters), false)); + } + } + + /// + /// Provides a new item for the region based on the supplied candidate target contract name. + /// + /// The target contract to build. + /// An instance of an item to put into the . + protected virtual object CreateNewRegionItem(string candidateTargetContract) + { + try + { + var view = ContainerLocator.Container.Resolve(candidateTargetContract); + + MvvmHelpers.AutowireViewModel(view); + + return view; + } + catch (ContainerResolutionException) + { + throw; + } + catch (Exception e) + { + throw new InvalidOperationException( + string.Format(CultureInfo.CurrentCulture, Resources.CannotCreateNavigationTarget, candidateTargetContract), + e); + } + } + + private class RegionCollection : IRegionCollection + { + private readonly IRegionManager regionManager; + private readonly List regions; + + public RegionCollection(IRegionManager regionManager) + { + this.regionManager = regionManager; + regions = new List(); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public IEnumerator GetEnumerator() + { + UpdateRegions(); + + return regions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IRegion this[string regionName] + { + get + { + UpdateRegions(); + + IRegion region = GetRegionByName(regionName); + if (region == null) + { + throw new KeyNotFoundException(string.Format(CultureInfo.CurrentUICulture, Resources.RegionNotInRegionManagerException, regionName)); + } + + return region; + } + } + + public void Add(IRegion region) + { + if (region == null) + throw new ArgumentNullException(nameof(region)); + + UpdateRegions(); + + if (region.Name == null) + { + throw new InvalidOperationException(Resources.RegionNameCannotBeEmptyException); + } + + if (GetRegionByName(region.Name) != null) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, + Resources.RegionNameExistsException, region.Name)); + } + + regions.Add(region); + region.RegionManager = regionManager; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, region, 0)); + } + + public bool Remove(string regionName) + { + UpdateRegions(); + + bool removed = false; + + IRegion region = GetRegionByName(regionName); + if (region != null) + { + removed = true; + regions.Remove(region); + region.RegionManager = null; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, region, 0)); + } + + return removed; + } + + public bool ContainsRegionWithName(string regionName) + { + UpdateRegions(); + + return GetRegionByName(regionName) != null; + } + + /// + /// Adds a region to the with the name received as argument. + /// + /// The name to be given to the region. + /// The region to be added to the . + /// Thrown if is . + /// Thrown if and 's name do not match and the is not . + public void Add(string regionName, IRegion region) + { + if (region == null) + throw new ArgumentNullException(nameof(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; + + Add(region); + } + + private IRegion GetRegionByName(string regionName) + { + return regions.FirstOrDefault(r => r.Name == regionName); + } + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + var handler = CollectionChanged; + + if (handler != null) + { + handler(this, notifyCollectionChangedEventArgs); + } + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs new file mode 100644 index 0000000000..2d7f6dcb40 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Controls; +using Prism.Common; +using Prism.Ioc; +using Prism.Ioc.Internals; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// + /// Implementation of that relies on a + /// to create new views when necessary. + /// + public class RegionNavigationContentLoader : IRegionNavigationContentLoader + { + private readonly IContainerExtension _container; + + /// + /// Initializes a new instance of the class with a service locator. + /// + /// The . + public RegionNavigationContentLoader(IContainerExtension container) + { + _container = container; + } + + /// + /// Gets the view to which the navigation request represented by applies. + /// + /// The region. + /// The context representing the navigation request. + /// + /// The view to be the target of the navigation request. + /// + /// + /// If none of the views in the region can be the target of the navigation request, a new view + /// is created and added to the region. + /// + /// when a new view cannot be created for the navigation request. + public object LoadContent(IRegion region, NavigationContext navigationContext) + { + if (region == null) + throw new ArgumentNullException(nameof(region)); + + if (navigationContext == null) + throw new ArgumentNullException(nameof(navigationContext)); + + string candidateTargetContract = GetContractFromNavigationContext(navigationContext); + + var candidates = GetCandidatesFromRegion(region, candidateTargetContract); + + var acceptingCandidates = + candidates.Where( + v => + { + if (v is IRegionAware navigationAware && !navigationAware.IsNavigationTarget(navigationContext)) + { + return false; + } + + if (!(v is Control control)) + { + return true; + } + + navigationAware = control.DataContext as IRegionAware; + return navigationAware == null || navigationAware.IsNavigationTarget(navigationContext); + }); + + var view = acceptingCandidates.FirstOrDefault(); + + if (view != null) + { + return view; + } + + view = CreateNewRegionItem(candidateTargetContract); + + AddViewToRegion(region, view); + + return view; + } + + /// + /// Adds the view to the region. + /// + /// The region to add the view to + /// The view to add to the region + protected virtual void AddViewToRegion(IRegion region, object view) + { + region.Add(view); + } + + /// + /// Provides a new item for the region based on the supplied candidate target contract name. + /// + /// The target contract to build. + /// An instance of an item to put into the . + protected virtual object CreateNewRegionItem(string candidateTargetContract) + { + try + { + var newRegionItem = _container.Resolve(candidateTargetContract); + MvvmHelpers.AutowireViewModel(newRegionItem); + return newRegionItem; + } + catch (ContainerResolutionException) + { + throw; + } + catch (Exception e) + { + throw new InvalidOperationException( + string.Format(CultureInfo.CurrentCulture, Resources.CannotCreateNavigationTarget, candidateTargetContract), + e); + } + } + + /// + /// Returns the candidate TargetContract based on the . + /// + /// The navigation contract. + /// The candidate contract to seek within the and to use, if not found, when resolving from the container. + protected virtual string GetContractFromNavigationContext(NavigationContext navigationContext) + { + if (navigationContext == null) throw new ArgumentNullException(nameof(navigationContext)); + + var candidateTargetContract = UriParsingHelper.GetAbsolutePath(navigationContext.Uri); + candidateTargetContract = candidateTargetContract.TrimStart('/'); + return candidateTargetContract; + } + + /// + /// Returns the set of candidates that may satisfy this navigation request. + /// + /// The region containing items that may satisfy the navigation request. + /// The candidate navigation target as determined by + /// An enumerable of candidate objects from the + protected virtual IEnumerable GetCandidatesFromRegion(IRegion region, string candidateNavigationContract) + { + if (region is null) + { + throw new ArgumentNullException(nameof(region)); + } + + if (string.IsNullOrEmpty(candidateNavigationContract)) + { + throw new ArgumentNullException(nameof(candidateNavigationContract)); + } + + var contractCandidates = GetCandidatesFromRegionViews(region, candidateNavigationContract); + + if (!contractCandidates.Any()) + { + var matchingType = _container.GetRegistrationType(candidateNavigationContract); + if (matchingType is null) + { + return Array.Empty(); + } + + return GetCandidatesFromRegionViews(region, matchingType.FullName); + } + + return contractCandidates; + } + + private IEnumerable GetCandidatesFromRegionViews(IRegion region, string candidateNavigationContract) + { + return region.Views.Where(v => v is not null && ViewIsMatch(v.GetType(), candidateNavigationContract)); + } + + private static bool ViewIsMatch(Type viewType, string navigationSegment) + { + var names = new[] { viewType.Name, viewType.FullName }; + return names.Any(x => x.Equals(navigationSegment, StringComparison.Ordinal)); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs new file mode 100644 index 0000000000..7ca44c99c8 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Avalonia.Controls; +using Prism.Common; +using Prism.Ioc; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// Provides navigation for regions. + public class RegionNavigationService : IRegionNavigationService + { + private readonly IContainerProvider _container; + private readonly IRegionNavigationContentLoader _regionNavigationContentLoader; + private NavigationContext _currentNavigationContext; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The navigation target handler. + /// The journal. + public RegionNavigationService(IContainerExtension container, IRegionNavigationContentLoader regionNavigationContentLoader, IRegionNavigationJournal journal) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _regionNavigationContentLoader = regionNavigationContentLoader ?? throw new ArgumentNullException(nameof(regionNavigationContentLoader)); + Journal = journal ?? throw new ArgumentNullException(nameof(journal)); + Journal.NavigationTarget = this; + } + + /// Gets or sets the region. + /// The region. + public IRegion Region { get; set; } + + /// Gets the journal. + /// The journal. + public IRegionNavigationJournal Journal { get; private set; } + + /// Raised when the region is about to be navigated to content. + public event EventHandler Navigating; + + private void RaiseNavigating(NavigationContext navigationContext) + { + Navigating?.Invoke(this, new RegionNavigationEventArgs(navigationContext)); + } + + /// Raised when the region is navigated to content. + public event EventHandler Navigated; + + private void RaiseNavigated(NavigationContext navigationContext) + { + Navigated?.Invoke(this, new RegionNavigationEventArgs(navigationContext)); + } + + /// Raised when a navigation request fails. + public event EventHandler NavigationFailed; + + private void RaiseNavigationFailed(NavigationContext navigationContext, Exception error) + { + NavigationFailed?.Invoke(this, new RegionNavigationFailedEventArgs(navigationContext, error)); + } + + /// Initiates navigation to the specified target. + /// The target. + /// A callback to execute when the navigation request is completed. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is marshalled to callback")] + public void RequestNavigate(Uri target, Action navigationCallback) + { + RequestNavigate(target, navigationCallback, null); + } + + /// Initiates navigation to the specified target. + /// The target. + /// A callback to execute when the navigation request is completed. + /// The navigation parameters specific to the navigation request. + public void RequestNavigate(Uri target, Action navigationCallback, INavigationParameters navigationParameters) + { + if (navigationCallback == null) + throw new ArgumentNullException(nameof(navigationCallback)); + + try + { + DoNavigate(target, navigationCallback, navigationParameters); + } + catch (Exception e) + { + NotifyNavigationFailed(new NavigationContext(this, target), navigationCallback, e); + } + } + + private void DoNavigate(Uri source, Action navigationCallback, INavigationParameters navigationParameters) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (Region == null) + throw new InvalidOperationException(Resources.NavigationServiceHasNoRegion); + + _currentNavigationContext = new NavigationContext(this, source, navigationParameters); + + // starts querying the active views + RequestCanNavigateFromOnCurrentlyActiveView( + _currentNavigationContext, + navigationCallback, + Region.ActiveViews.ToArray(), + 0); + } + + private void RequestCanNavigateFromOnCurrentlyActiveView( + NavigationContext navigationContext, + Action navigationCallback, + object[] activeViews, + int currentViewIndex) + { + if (currentViewIndex < activeViews.Length) + { + if (activeViews[currentViewIndex] is IConfirmNavigationRequest vetoingView) + { + // the current active view implements IConfirmNavigationRequest, request confirmation + // providing a callback to resume the navigation request + vetoingView.ConfirmNavigationRequest( + navigationContext, + canNavigate => + { + if (_currentNavigationContext == navigationContext && canNavigate) + { + RequestCanNavigateFromOnCurrentlyActiveViewModel( + navigationContext, + navigationCallback, + activeViews, + currentViewIndex); + } + else + { + NotifyNavigationFailed(navigationContext, navigationCallback, null); + } + }); + } + else + { + RequestCanNavigateFromOnCurrentlyActiveViewModel( + navigationContext, + navigationCallback, + activeViews, + currentViewIndex); + } + } + else + { + ExecuteNavigation(navigationContext, activeViews, navigationCallback); + } + } + + private void RequestCanNavigateFromOnCurrentlyActiveViewModel( + NavigationContext navigationContext, + Action navigationCallback, + object[] activeViews, + int currentViewIndex) + { + if (activeViews[currentViewIndex] is Control control) + { + if (control.DataContext is IConfirmNavigationRequest vetoingViewModel) + { + // the data model for the current active view implements IConfirmNavigationRequest, request confirmation + // providing a callback to resume the navigation request + vetoingViewModel.ConfirmNavigationRequest( + navigationContext, + canNavigate => + { + if (_currentNavigationContext == navigationContext && canNavigate) + { + RequestCanNavigateFromOnCurrentlyActiveView( + navigationContext, + navigationCallback, + activeViews, + currentViewIndex + 1); + } + else + { + NotifyNavigationFailed(navigationContext, navigationCallback, null); + } + }); + + return; + } + } + + RequestCanNavigateFromOnCurrentlyActiveView( + navigationContext, + navigationCallback, + activeViews, + currentViewIndex + 1); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is marshalled to callback")] + private void ExecuteNavigation(NavigationContext navigationContext, object[] activeViews, Action navigationCallback) + { + try + { + NotifyActiveViewsNavigatingFrom(navigationContext, activeViews); + + object view = _regionNavigationContentLoader.LoadContent(Region, navigationContext); + + // Raise the navigating event just before activating the view. + RaiseNavigating(navigationContext); + + Region.Activate(view); + + // Update the navigation journal before notifying others of navigation + IRegionNavigationJournalEntry journalEntry = _container.Resolve(); + journalEntry.Uri = navigationContext.Uri; + journalEntry.Parameters = navigationContext.Parameters; + + bool persistInHistory = PersistInHistory(view); + + Journal.RecordNavigation(journalEntry, persistInHistory); + + // The view can be informed of navigation + Action action = (n) => n.OnNavigatedTo(navigationContext); + MvvmHelpers.ViewAndViewModelAction(view, action); + + navigationCallback(new NavigationResult(navigationContext, true)); + + // Raise the navigated event when navigation is completed. + RaiseNavigated(navigationContext); + } + catch (Exception e) + { + NotifyNavigationFailed(navigationContext, navigationCallback, e); + } + } + + private static bool PersistInHistory(object view) + { + bool persist = true; + MvvmHelpers.ViewAndViewModelAction(view, ija => { persist &= ija.PersistInHistory(); }); + return persist; + } + + private void NotifyNavigationFailed(NavigationContext navigationContext, Action navigationCallback, Exception e) + { + var navigationResult = + e != null ? new NavigationResult(navigationContext, e) : new NavigationResult(navigationContext, false); + + navigationCallback(navigationResult); + RaiseNavigationFailed(navigationContext, e); + } + + private static void NotifyActiveViewsNavigatingFrom(NavigationContext navigationContext, object[] activeViews) + { + InvokeOnNavigationAwareElements(activeViews, (n) => n.OnNavigatedFrom(navigationContext)); + } + + private static void InvokeOnNavigationAwareElements(IEnumerable items, Action invocation) + { + foreach (var item in items) + { + MvvmHelpers.ViewAndViewModelAction(item, invocation); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs new file mode 100644 index 0000000000..142cf17d00 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Prism.Common; +using Prism.Events; +using Prism.Ioc; +using Prism.Properties; + +namespace Prism.Navigation.Regions +{ + /// + /// Defines a registry for the content of the regions used on View Discovery composition. + /// + public class RegionViewRegistry : IRegionViewRegistry + { + private readonly IContainerProvider _container; + private readonly ListDictionary> _registeredContent = new ListDictionary>(); + private readonly WeakDelegatesManager _contentRegisteredListeners = new WeakDelegatesManager(); + + /// Creates a new instance of the class. + /// used to create the instance of the views from its . + public RegionViewRegistry(IContainerExtension container) + { + _container = container; + } + + /// Occurs whenever a new view is registered. + public event EventHandler ContentRegistered + { + add => _contentRegisteredListeners.AddListener(value); + remove => _contentRegisteredListeners.RemoveListener(value); + } + + /// Returns the contents registered for a region. + /// Name of the region which content is being requested. + /// The to use to get the View. + /// Collection of contents registered for the region. + public IEnumerable GetContents(string regionName, IContainerProvider container) + { + var items = new List(); + foreach (Func getContentDelegate in _registeredContent[regionName]) + { + items.Add(getContentDelegate(container)); + } + + return items; + } + + /// Registers a content type with a region name. + /// Region name to which the will be registered. + /// Content type to be registered for the . + public void RegisterViewWithRegion(string regionName, Type viewType) + { + RegisterViewWithRegion(regionName, _ => CreateInstance(viewType)); + } + + /// Registers a delegate that can be used to retrieve the content associated with a region name. + /// Region name to which the will be registered. + /// Delegate used to retrieve the content associated with the . + public void RegisterViewWithRegion(string regionName, Func getContentDelegate) + { + _registeredContent.Add(regionName, getContentDelegate); + OnContentRegistered(new ViewRegisteredEventArgs(regionName, getContentDelegate)); + } + + /// + /// Associate a view with a region, by registering a type. When the region get's displayed + /// this type will be resolved using the ServiceLocator into a concrete instance. The instance + /// will be added to the Views collection of the region + /// + /// The name of the region to associate the view with. + /// The type of the view to register with the + /// The , for adding several views easily + public void RegisterViewWithRegion(string regionName, string targetName) => + RegisterViewWithRegion(regionName, c => c.Resolve(targetName)); + + /// Creates an instance of a registered view . + /// Type of the registered view. + /// Instance of the registered view. + protected virtual object CreateInstance(Type type) + { + var view = _container.Resolve(type); + MvvmHelpers.AutowireViewModel(view); + return view; + } + + private void OnContentRegistered(ViewRegisteredEventArgs e) + { + // TODO (2022-11-28): Consider returning an object with Success(bool) and Exception(ex) + try + { + _contentRegisteredListeners.Raise(this, e); + } + catch (TargetInvocationException ex) + { + Exception rootException; + if (ex.InnerException != null) + { + rootException = ex.InnerException.GetRootException(); + } + else + { + rootException = ex.GetRootException(); + } + + // TODO (2022-11-28): Consider safely informing user of XAML error when encountered + throw new ViewRegistrationException(string.Format(CultureInfo.CurrentCulture, + Resources.OnViewRegisteredException, e.RegionName, rootException), ex.InnerException); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs new file mode 100644 index 0000000000..a55ec6ad38 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs @@ -0,0 +1,70 @@ +// TODO: Feature currently disabled +/* +using Prism.Navigation.Regions.Behaviors; +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Styling; + +namespace Prism.Navigation.Regions +{ + /// + /// Adapter that creates a new and binds all + /// the views to the adapted . + /// It also keeps the and the selected items + /// of the in sync. + /// + public class SelectorRegionAdapter : RegionAdapterBase + { + /// + /// Initializes a new instance of . + /// + /// The factory used to create the region behaviors to attach to the created regions. + public SelectorRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) + : base(regionBehaviorFactory) + { + } + + /// + /// Adapts an to an . + /// + /// The new region being used. + /// The object to adapt. + protected override void Adapt(IRegion region, Selector regionTarget) + { + } + + /// + /// Attach new behaviors. + /// + /// The region being used. + /// The object to adapt. + /// + /// This class attaches the base behaviors and also listens for changes in the + /// activity of the region or the control selection and keeps the in sync. + /// + protected override void AttachBehaviors(IRegion region, Selector regionTarget) + { + if (region == null) + throw new ArgumentNullException(nameof(region)); + + // Add the behavior that syncs the items source items with the rest of the items + region.Behaviors.Add(SelectorItemsSourceSyncBehavior.BehaviorKey, new SelectorItemsSourceSyncBehavior() + { + /////HostControl = regionTarget + HostControl = regionTarget.SelectedItem as Avalonia.AvaloniaObject // TODO: Verify '.SelectedItem ...' + }); + + base.AttachBehaviors(region, regionTarget); + } + + /// + /// Creates a new instance of . + /// + /// A new instance of . + protected override IRegion CreateRegion() + { + return new Region(); + } + } +} +*/ diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs new file mode 100644 index 0000000000..0b44f93652 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs @@ -0,0 +1,28 @@ +using System.Linq; + +namespace Prism.Navigation.Regions +{ + /// + /// Region that allows a maximum of one active view at a time. + /// + public class SingleActiveRegion : Region + { + /// + /// Marks the specified view as active. + /// + /// The view to activate. + /// If there is an active view before calling this method, + /// that view will be deactivated automatically. + public override void Activate(object view) + { + object currentActiveView = ActiveViews.FirstOrDefault(); + + if (currentActiveView != null && currentActiveView != view && Views.Contains(currentActiveView)) + { + base.Deactivate(currentActiveView); + } + + base.Activate(view); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs new file mode 100644 index 0000000000..1b2965b62e --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace Prism.Navigation.Regions +{ + /// + /// Implementation of that takes an of + /// and filters it to display an collection of + /// elements (the items which the wraps). + /// + public class ViewsCollection : IViewsCollection + { + private readonly ObservableCollection subjectCollection; + + private readonly Dictionary monitoredItems = + new Dictionary(); + + private readonly Predicate filter; + private Comparison sort; + private List filteredItems = new List(); + + /// Initializes a new instance of the class. + /// The list to wrap and filter. + /// A predicate to filter the collection. + public ViewsCollection(ObservableCollection list, Predicate filter) + { + subjectCollection = list; + this.filter = filter; + MonitorAllMetadataItems(); + subjectCollection.CollectionChanged += SourceCollectionChanged; + UpdateFilteredItemsList(); + } + + /// Occurs when the collection changes. + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// Gets or sets the comparison used to sort the views. + /// The comparison to use. + public Comparison SortComparison + { + get { return sort; } + set + { + if (sort != value) + { + sort = value; + UpdateFilteredItemsList(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } + } + + /// Determines whether the collection contains a specific value. + /// The object to locate in the collection. + /// if is found in the collection; otherwise, . + public bool Contains(object value) + { + return filteredItems.Contains(value); + } + + ///Returns an enumerator that iterates through the collection.summary> + /// + ///A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return filteredItems.GetEnumerator(); + } + + ///Returns an enumerator that iterates through a collection.summary> + /// + ///An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// Used to invoked the event. + /// + private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + private void NotifyReset() + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// Removes all monitoring of underlying MetadataItems and re-adds them. + private void ResetAllMonitors() + { + RemoveAllMetadataMonitors(); + MonitorAllMetadataItems(); + } + + /// Adds all underlying MetadataItems to the list from the subjectCollection + private void MonitorAllMetadataItems() + { + foreach (var item in subjectCollection) + { + AddMetadataMonitor(item, filter(item)); + } + } + + /// Removes all monitored items from our monitoring list. + private void RemoveAllMetadataMonitors() + { + foreach (var item in monitoredItems) + { + item.Key.MetadataChanged -= OnItemMetadataChanged; + } + + monitoredItems.Clear(); + } + + /// Adds handler to monitor the MetadataItem and adds it to our monitoring list. + /// + /// + private void AddMetadataMonitor(ItemMetadata itemMetadata, bool isInList) + { + itemMetadata.MetadataChanged += OnItemMetadataChanged; + monitoredItems.Add( + itemMetadata, + new MonitorInfo + { + IsInList = isInList + }); + } + + /// Unhooks from the MetadataItem change event and removes from our monitoring list. + /// + private void RemoveMetadataMonitor(ItemMetadata itemMetadata) + { + itemMetadata.MetadataChanged -= OnItemMetadataChanged; + monitoredItems.Remove(itemMetadata); + } + + /// Invoked when any of the underlying ItemMetadata items we're monitoring changes. + /// + /// + private void OnItemMetadataChanged(object sender, EventArgs e) + { + ItemMetadata itemMetadata = (ItemMetadata)sender; + + // Our monitored item may have been removed during another event before + // our OnItemMetadataChanged got called back, so it's not unexpected + // that we may not have it in our list. + MonitorInfo monitorInfo; + bool foundInfo = monitoredItems.TryGetValue(itemMetadata, out monitorInfo); + if (!foundInfo) return; + + if (filter(itemMetadata)) + { + if (!monitorInfo.IsInList) + { + // This passes our filter and wasn't marked + // as in our list so we can consider this + // an Add. + monitorInfo.IsInList = true; + UpdateFilteredItemsList(); + NotifyAdd(itemMetadata.Item); + } + } + else + { + // This doesn't fit our filter, we remove from our + // tracking list, but should not remove any monitoring in + // case it fits our filter in the future. + monitorInfo.IsInList = false; + RemoveFromFilteredList(itemMetadata.Item); + } + } + + /// + /// The event handler due to changes in the underlying collection. + /// + /// + /// + private void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + UpdateFilteredItemsList(); + foreach (ItemMetadata itemMetadata in e.NewItems) + { + bool isInFilter = filter(itemMetadata); + AddMetadataMonitor(itemMetadata, isInFilter); + if (isInFilter) + { + NotifyAdd(itemMetadata.Item); + } + } + + // If we're sorting we can't predict how + // the collection has changed on an add so we + // resort to a reset notification. + if (sort != null) + { + NotifyReset(); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (ItemMetadata itemMetadata in e.OldItems) + { + RemoveMetadataMonitor(itemMetadata); + if (filter(itemMetadata)) + { + RemoveFromFilteredList(itemMetadata.Item); + } + } + + break; + + default: + ResetAllMonitors(); + UpdateFilteredItemsList(); + NotifyReset(); + + break; + } + } + + private void NotifyAdd(object item) + { + int newIndex = filteredItems.IndexOf(item); + NotifyAdd(new[] { item }, newIndex); + } + + private void RemoveFromFilteredList(object item) + { + int index = filteredItems.IndexOf(item); + UpdateFilteredItemsList(); + NotifyRemove(new[] { item }, index); + } + + private void UpdateFilteredItemsList() + { + filteredItems = subjectCollection.Where(i => filter(i)).Select(i => i.Item) + .OrderBy(o => o, new RegionItemComparer(SortComparison)).ToList(); + } + + private class MonitorInfo + { + public bool IsInList { get; set; } + } + + private class RegionItemComparer : Comparer + { + private readonly Comparison comparer; + + public RegionItemComparer(Comparison comparer) + { + this.comparer = comparer; + } + + public override int Compare(object x, object y) + { + if (comparer == null) + { + return 0; + } + + return comparer(x, y); + } + } + + private void NotifyAdd(IList items, int newStartingIndex) + { + if (items.Count > 0) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + items, + newStartingIndex)); + } + } + + private void NotifyRemove(IList items, int originalIndex) + { + if (items.Count > 0) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + items, + originalIndex)); + } + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj b/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj new file mode 100644 index 0000000000..ccd31efc1a --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj @@ -0,0 +1,54 @@ + + + + + + + + + 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;mvvm;xaml;avalonia;navigation;dialog;prismavalonia; + Copyright (c) 2024 Xeno Innovations, Inc. + Damian Suess, Suess Labs, various contributors + Prism.Avalonia + README.md + Prism.Avalonia.png + + + + + + + + + + + + + + + + + True + \ + + + True + \ + + + + + + + + + + + + diff --git a/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs b/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs new file mode 100644 index 0000000000..76856a7996 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs @@ -0,0 +1,187 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Prism.Common; +using Prism.Ioc; +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; + + // FROM Prism.Avalonia7.1.2 + 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(); + } + + 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..2503d2645a --- /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"); + + 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/PrismInitializationExtensions.cs b/src/Avalonia/Prism.Avalonia/PrismInitializationExtensions.cs new file mode 100644 index 0000000000..dc17adb593 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/PrismInitializationExtensions.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls; +using Prism.Events; +using Prism.Ioc; +using Prism.Modularity; +using Prism.Mvvm; +using Prism.Dialogs; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; + +namespace Prism +{ + internal static class PrismInitializationExtensions + { + internal static void ConfigureViewModelLocator() + { + ViewModelLocationProvider.SetDefaultViewModelFactory((view, type) => + { + return ContainerLocator.Container.Resolve(type); + }); + } + + internal static void RegisterRequiredTypes(this IContainerRegistry containerRegistry, IModuleCatalog moduleCatalog) + { + containerRegistry.RegisterInstance(moduleCatalog); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.Register(); + containerRegistry.Register(); + containerRegistry.Register(); + containerRegistry.Register(); //default dialog host + } + + internal static void RegisterDefaultRegionBehaviors(this IRegionBehaviorFactory regionBehaviors) + { + //// Avalonia to WPF Equivilant: BindRegionContextToAvaloniaObjectBehavior == BindRegionContextToDependencyObjectBehavior + regionBehaviors.AddIfMissing(BindRegionContextToAvaloniaObjectBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(RegionActiveAwareBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(SyncRegionContextWithHostBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(RegionManagerRegistrationBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(RegionMemberLifetimeBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(ClearChildViewsRegionBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(AutoPopulateRegionBehavior.BehaviorKey); + regionBehaviors.AddIfMissing(DestructibleRegionBehavior.BehaviorKey); + } + + internal static void RegisterDefaultRegionAdapterMappings(this RegionAdapterMappings regionAdapterMappings) + { + //// regionAdapterMappings.RegisterMapping(); + regionAdapterMappings.RegisterMapping(); + regionAdapterMappings.RegisterMapping(); + } + + internal static void RunModuleManager(IContainerProvider containerProvider) + { + IModuleManager manager = containerProvider.Resolve(); + manager.Run(); + } + } +} diff --git a/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs b/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d24a6afdc4 --- /dev/null +++ b/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Metadata; + +[assembly: ComVisible(false)] +[assembly: CLSCompliant(true)] + +[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")] 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..0d55e231c1 --- /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..0cd2cda19f --- /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..dd563d61d7 --- /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..033d7a5e9e --- /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/GlobalSuppressions.cs b/src/Avalonia/Prism.DryIoc.Avalonia/GlobalSuppressions.cs new file mode 100644 index 0000000000..a69d206a20 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/GlobalSuppressions.cs @@ -0,0 +1,11 @@ +// 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 +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Error List, point to "Suppress Message(s)", and click +// "In Project Suppression File". +// You do not need to add suppressions to this file manually. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames")] 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..9b31122e27 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj @@ -0,0 +1,38 @@ + + + + + + + + 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 + \ + + + + + + + + + + + + diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs b/src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs new file mode 100644 index 0000000000..953ae7793b --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs @@ -0,0 +1,38 @@ +using System; +using DryIoc; +using Prism.Container.DryIoc; +using Prism.Ioc; +using ExceptionExtensions = System.ExceptionExtensions; + +namespace Prism.DryIoc +{ + /// + /// Base application class that uses as it's container. + /// + public abstract class PrismApplication : PrismApplicationBase + { + /// + /// Create to alter behavior of + /// + /// An instance of + protected virtual Rules CreateContainerRules() => DryIocContainerExtension.DefaultRules; + + /// + /// Create a new used by Prism. + /// + /// A new . + protected override IContainerExtension CreateContainerExtension() + { + return new DryIocContainerExtension(CreateContainerRules()); + } + + /// + /// Registers the s of the Exceptions that are not considered + /// root exceptions by the . + /// + protected override void RegisterFrameworkExceptionTypes() + { + ExceptionExtensions.RegisterFrameworkExceptionType(typeof(ContainerException)); + } + } +} diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs b/src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs new file mode 100644 index 0000000000..e9d0b021e0 --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs @@ -0,0 +1,31 @@ +using System; +using DryIoc; +using Prism.Container.DryIoc; +using Prism.Ioc; + +namespace Prism.DryIoc +{ + /// Base bootstrapper class that uses as it's container. + public abstract class PrismBootstrapper : PrismBootstrapperBase + { + /// Create to alter behavior of + /// An instance of + protected virtual Rules CreateContainerRules() => DryIocContainerExtension.DefaultRules; + + /// Create a new used by Prism. + /// A new . + protected override IContainerExtension CreateContainerExtension() + { + return new DryIocContainerExtension(CreateContainerRules()); + } + + /// + /// Registers the s of the Exceptions that are not considered + /// root exceptions by the . + /// + protected override void RegisterFrameworkExceptionTypes() + { + ExceptionExtensions.RegisterFrameworkExceptionType(typeof(ContainerException)); + } + } +} 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..7d36c4f34f --- /dev/null +++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +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..7fdd419bff --- /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..7ec61e1f8a --- /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..f85cca105a --- /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..8c070b6c43 --- /dev/null +++ b/src/Avalonia/ReadMe.md @@ -0,0 +1,16 @@ + +Thank you for installing Prism.Avalonia 8.1! + +## 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/AvaloniaCommunity/Prism.Avalonia 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/AvaloniaCommunity/Prism.Avalonia/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/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..2c2138ec0d --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs @@ -0,0 +1,191 @@ +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 +{ + 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..0d3e8b1112 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs @@ -0,0 +1,28 @@ +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..633b3fdfda --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs @@ -0,0 +1,169 @@ +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..1eef597648 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs @@ -0,0 +1,383 @@ +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)this.GetValue(AutoEnableProperty); } + set { this.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) + { + this.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/ListDictionaryFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs new file mode 100644 index 0000000000..af44c38436 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs @@ -0,0 +1,260 @@ +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..8e6d558824 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockAsyncModuleTypeLoader.cs @@ -0,0 +1,49 @@ +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); + + this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, CallbackArgumentError)); + callbackEvent.Set(); + }); + retrieverThread.Start(); + } + + public event EventHandler ModuleDownloadProgressChanged; + + private void RaiseLoadModuleProgressChanged(ModuleDownloadProgressChangedEventArgs e) + { + this.ModuleDownloadProgressChanged?.Invoke(this, e); + } + + public event EventHandler LoadModuleCompleted; + + private void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) + { + this.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..7dc3eeb3cb --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockCommand.cs @@ -0,0 +1,34 @@ +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 (this.CanExecuteChanged != null) + this.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..67d89c60f3 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockContainerAdapter.cs @@ -0,0 +1,142 @@ +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 (!this.ResolvedInstances.ContainsKey(type)) + { + resolvedInstance = Activator.CreateInstance(type); + this.ResolvedInstances.Add(type, resolvedInstance); + } + else + { + resolvedInstance = this.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..a543609cb1 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockDelegateReference.cs @@ -0,0 +1,18 @@ +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..0b33b4ed50 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockHostAwareRegionBehavior.cs @@ -0,0 +1,17 @@ +using Avalonia; +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..cad117e2fa --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockModuleTypeLoader.cs @@ -0,0 +1,41 @@ +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) + { + this.LoadedModules.Add(moduleInfo); + this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, this.LoadCompletedError)); + } + + public event EventHandler ModuleDownloadProgressChanged; + + public void RaiseLoadModuleProgressChanged(ModuleDownloadProgressChangedEventArgs e) + { + this.ModuleDownloadProgressChanged?.Invoke(this, e); + } + + public event EventHandler LoadModuleCompleted; + + public void RaiseLoadModuleCompleted(ModuleInfo moduleInfo, Exception error) + { + this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, error)); + } + + public void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) + { + this.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..b7b4ee4aa4 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockPresentationRegion.cs @@ -0,0 +1,142 @@ +using System.ComponentModel; + +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..5760933aee --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegion.cs @@ -0,0 +1,114 @@ +using System.ComponentModel; + +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) + { + this.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..50257209c0 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionAdapter.cs @@ -0,0 +1,24 @@ +using Avalonia; + +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 (this.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..f80e5fc762 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehavior.cs @@ -0,0 +1,19 @@ +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..bd7803b3e7 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionBehaviorCollection.cs @@ -0,0 +1,12 @@ +using System.Collections; + +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..8442fee3c9 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManager.cs @@ -0,0 +1,136 @@ +using System.Collections; + +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) + { + this.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..9f2470ecf8 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockRegionManagerAccessor.cs @@ -0,0 +1,40 @@ +using Avalonia; + +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 this.GetRegionName(element); + } + + IRegionManager IRegionManagerAccessor.GetRegionManager(AvaloniaObject element) + { + if (this.GetRegionManager != null) + { + return this.GetRegionManager(element); + } + + return null; + } + + public void UpdateRegions() + { + if (this.UpdatingRegions != null) + { + this.UpdatingRegions(this, EventArgs.Empty); + } + } + + public int GetSubscribersCount() + { + return this.UpdatingRegions != null ? this.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..9d49f948c1 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockSortableViews.cs @@ -0,0 +1,17 @@ +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..eb3a4f8633 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/MockViewsCollection.cs @@ -0,0 +1,38 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +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..430e2e7cb7 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockAttributedModule.cs @@ -0,0 +1,19 @@ +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..8222ed91f9 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependantModule.cs @@ -0,0 +1,20 @@ +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..f7d2eb3d0d --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockDependencyModule.cs @@ -0,0 +1,19 @@ +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..5254a85d0f --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockExposingTypeFromGacAssemblyModule.cs @@ -0,0 +1,36 @@ +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..8f32bfb033 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleA.cs @@ -0,0 +1,22 @@ +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..5cce67a0b8 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingAssembly.cs @@ -0,0 +1,18 @@ +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..5df9904dfa --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleReferencingOtherModule.cs @@ -0,0 +1,21 @@ +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..ccf198977a --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Mocks/Modules/MockModuleThrowingException.cs @@ -0,0 +1,18 @@ +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..b03cfb17f3 --- /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..820f0dcffc --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/AssemblyResolverFixture.Desktop.cs @@ -0,0 +1,127 @@ +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] + 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] + 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..22ef329b0c --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ConfigurationModuleCatalogFixture.Desktop.cs @@ -0,0 +1,160 @@ +using System.Configuration; +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..57381d10ba --- /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.Wpf.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..58e026ddc9 --- /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.Wpf.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.Wpf.Tests.Mocks.Modules.MockModuleA", modules[0].ModuleType); + } + + [Fact] + public void ShouldCorrectlyEscapeRef() + { + string assemblyPath = ModulesDirectory6 + @"\Mock Module #.dll"; + CompilerHelper.CompileFile(@"Prism.Wpf.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.Wpf.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.Wpf.Tests.Mocks.Modules.MockModuleA"); + //} + + [Fact] + public void ShouldNotThrowWithLoadFromByteAssemblies() + { + CompilerHelper.CleanUpDirectory(@".\CompileOutput\"); + CompilerHelper.CleanUpDirectory(@".\IgnoreLoadFromByteAssembliesTestDir\"); + var results = CompilerHelper.CompileFile(@"Prism.Wpf.Tests.Mocks.Modules.MockModuleA.cs", + @".\CompileOutput\MockModuleA.dll"); + + CompilerHelper.CompileFile(@"Prism.Wpf.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.Wpf.Tests.Mocks.Modules.MockAttributedModule") >= 0) + ); + } + finally + { + if (testDomain != null) + AppDomain.Unload(testDomain); + } + } + + [Fact] + public void ShouldGetModuleNameFromAttribute() + { + CompilerHelper.CompileFile(@"Prism.Wpf.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.Wpf.Tests.Mocks.Modules.MockDependencyModule.cs", + ModulesDirectory3 + @"\DependencyModule.dll"); + + CompilerHelper.CompileFile(@"Prism.Wpf.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.Wpf.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.Wpf.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.Wpf.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.Wpf.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.Wpf.Tests.Mocks.Modules.MockModuleA.cs", + ModulesDirectory4 + @"\MockModuleZZZ.dll"); + + CompilerHelper.CompileFile(@"Prism.Wpf.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.Wpf.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.Wpf.Tests.Mocks.Modules.MockAttributedModule") >= 0) + ); + } + finally + { + if (testDomain != null) + AppDomain.Unload(testDomain); + } + } + + [Fact] + public void ShouldLoadAssemblyEvenIfIsExposingTypesFromAnAssemblyInTheGac() + { + CompilerHelper.CompileFile(@"Prism.Wpf.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.Wpf.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.Wpf.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..c42034e5ae --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleCatalogFixture.cs @@ -0,0 +1,478 @@ +using System.Collections.ObjectModel; +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..527863d6f7 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Modularity/ModuleInfoGroupExtensionsFixture.cs @@ -0,0 +1,82 @@ +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..5c5bff60c9 --- /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(DisplayName = "Flaky test, 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..f96eaea69d --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Prism.Avalonia.Tests.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + 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..af7fc923ad --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/PrismApplicationBaseFixture.cs @@ -0,0 +1,350 @@ +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; + +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..9501539c57 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/PrismBootstrapperBaseFixture.cs @@ -0,0 +1,344 @@ +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; + +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(); + } + } + + 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..5a6a720220 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/AllActiveRegionFixture.cs @@ -0,0 +1,32 @@ +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..f72885919e --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -0,0 +1,124 @@ +using Moq; +using Prism.Avalonia.Tests.Mocks; +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; + this.GetContentsArgumentRegionName = regionName; + return this.GetContentsReturnValue; + } + + public void RaiseContentRegistered(string regionName, object view) + { + this.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..fbded17b99 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehaviorFixture.cs @@ -0,0 +1,97 @@ +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class BindRegionContextToAvaloniaObjectBehaviorFixture + { + [StaFact] + 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] + 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] + 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] + 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..644ffe1136 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/ClearChildViewsRegionBehaviorFixture.cs @@ -0,0 +1,79 @@ +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..729b795c32 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/DelayedRegionCreationBehaviorFixture.cs @@ -0,0 +1,198 @@ +using Avalonia; +using Prism.Avalonia.Tests.Mocks; +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 = this.GetBehavior(control1, accessor, adapter); + var behavior2 = this.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 = this.GetBehavior(control1, accessor); + behavior1.Attach(); + var behavior2 = this.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 = this.GetBehavior(control, accessor); + behavior.Attach(); + accessor.UpdateRegions(); + IRegion region = RegionManager.GetObservableRegion(control).Value; + + accessor.UpdateRegions(); + + Assert.Same(region, RegionManager.GetObservableRegion(control).Value); + } + + [StaFact] + public void BehaviorDoesNotPreventControlFromBeingGarbageCollected() + { + var control = new MockFrameworkElement(); + WeakReference controlWeakReference = new WeakReference(control); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = this.GetBehavior(control, accessor); + behavior.Attach(); + + Assert.True(controlWeakReference.IsAlive); + GC.KeepAlive(control); + + control = null; + GC.Collect(); + + Assert.False(controlWeakReference.IsAlive); + } + + [StaFact] + public void BehaviorDoesNotPreventControlFromBeingGarbageCollectedWhenRegionWasCreated() + { + var control = new MockFrameworkElement(); + WeakReference controlWeakReference = new WeakReference(control); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = this.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 = this.GetBehavior(control, accessor); + behavior.Attach(); + + int startingCount = accessor.GetSubscribersCount(); + + behavior.Detach(); + + Assert.Equal(startingCount - 1, accessor.GetSubscribersCount()); + } + + [StaFact] + public void ShouldCleanupBehaviorOnceRegionIsCreated() + { + var control = new MockFrameworkElement(); + var control2 = new MockFrameworkContentElement(); + + var accessor = new MockRegionManagerAccessor + { + GetRegionName = d => "myRegionName" + }; + + var behavior = this.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 = this.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..e6f8e97928 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionActiveAwareBehaviorFixture.cs @@ -0,0 +1,330 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Moq; +using Prism.Avalonia.Tests.Mocks; +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..4c8da50714 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionManagerRegistrationBehaviorFixture.cs @@ -0,0 +1,358 @@ +using System.Collections; +using Avalonia.Controls; +using Prism.Avalonia.Tests.Mocks; +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] + 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] + 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] + 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 this.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..0c7d948684 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/RegionMemberLifetimeBehaviorFixture.cs @@ -0,0 +1,259 @@ +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class RegionMemberLifetimeBehaviorFixture + { + protected Region Region { get; set; } + protected RegionMemberLifetimeBehavior Behavior { get; set; } + + 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..e15b27c897 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/SyncRegionContextWithHostBehaviorFixture.cs @@ -0,0 +1,139 @@ +using Avalonia; +using Prism.Avalonia.Tests.Mocks; +using Prism.Common; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Avalonia.Tests.Regions.Behaviors +{ + public class SyncRegionContextWithHostBehaviorFixture + { + [StaFact] + 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] + 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] + 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] + 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] + 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..a060f40ea6 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ContentControlRegionAdapterFixture.cs @@ -0,0 +1,161 @@ +using Avalonia.Controls; +using Avalonia.Data; +using Prism.Avalonia.Tests.Mocks; +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..a78dad1058 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/LocatorNavigationTargetHandlerFixture.cs @@ -0,0 +1,343 @@ +using Avalonia.Controls; +using Moq; +using Prism.Ioc; +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..ca769c28cd --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationAsyncExtensionsFixture.cs @@ -0,0 +1,104 @@ +using Moq; +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..30122cdcb2 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/NavigationContextFixture.cs @@ -0,0 +1,45 @@ +using Moq; +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..0e3d06d8f4 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterBaseFixture.cs @@ -0,0 +1,108 @@ +using Prism.Avalonia.Tests.Mocks; +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..b6a8d35859 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionAdapterMappingsFixture.cs @@ -0,0 +1,146 @@ +using Prism.Navigation.Regions; +using Prism.Avalonia.Tests.Mocks; +using Avalonia.Controls; +using Moq; +using Prism.Ioc; +using Xunit; + +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..c6faeebf5a --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorCollectionFixture.cs @@ -0,0 +1,40 @@ +using Prism.Avalonia.Tests.Mocks; +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..725855a108 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFactoryFixture.cs @@ -0,0 +1,72 @@ +using Prism.Avalonia.Tests.Mocks; +using Moq; +using Prism.Ioc; +using Xunit; + +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..012cc481b5 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionBehaviorFixture.cs @@ -0,0 +1,56 @@ +using Prism.Avalonia.Tests.Mocks; +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..15b0e803cc --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionFixture.cs @@ -0,0 +1,647 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using Moq; +using Prism.Ioc; +using Prism.Avalonia.Tests.Mocks; +using Xunit; + +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..44434a7b26 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs @@ -0,0 +1,499 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Moq; +using Prism.Avalonia.Tests.Mocks; +using Prism.Ioc; +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] + 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] + 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..8a31b4fc58 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerRequestNavigateFixture.cs @@ -0,0 +1,126 @@ +using Moq; +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..1217da8331 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationJournalFixture.cs @@ -0,0 +1,487 @@ +using Moq; +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..25346c7c41 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionNavigationServiceFixture.new.cs @@ -0,0 +1,1193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using Avalonia.Controls; +using Moq; +using Prism.Ioc; +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] + 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..2cd1e3ea62 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs @@ -0,0 +1,198 @@ +using Avalonia.Controls; +using Moq; +using Prism.Avalonia.Tests.Mvvm; +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.Equal(typeof(MockContentObject), calledType); + 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] + 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] + 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.Null(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..2df2817970 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/SingleActiveRegionFixture.cs @@ -0,0 +1,28 @@ +using Moq; +using Prism.Ioc; +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..ea3c51df75 --- /dev/null +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/ViewsCollectionFixture.cs @@ -0,0 +1,344 @@ +// 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 Prism.Avalonia.Tests.Mocks; +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.DryIoc.Avalonia.Tests/DryIocBootstrapperFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperFixture.cs new file mode 100644 index 0000000000..4b011b1bc0 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperFixture.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using DryIoc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Prism.IocContainer.Avalonia.Tests.Support; +using Prism.IocContainer.Avalonia.Tests.Support.Mocks; +using Prism.Logging; +using Prism.Modularity; +using Prism.Regions; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperFixture : BootstrapperFixtureBase + { + [TestMethod] + public void ContainerDefaultsToNull() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + var container = bootstrapper.BaseContainer; + + Assert.IsNull(container); + } + + [TestMethod] + public void CanCreateConcreteBootstrapper() + { + new DefaultDryIocBootstrapper(); + } + + [TestMethod] + public void CreateContainerShouldInitializeContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + IContainer container = bootstrapper.CallCreateContainer(); + + Assert.IsNotNull(container); + Assert.IsInstanceOfType(container, typeof(IContainer)); + } + + [TestMethod] + public void ConfigureContainerAddsModuleCatalogToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + var returnedCatalog = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(returnedCatalog); + Assert.IsTrue(returnedCatalog is ModuleCatalog); + } + + [TestMethod] + public void ConfigureContainerAddsLoggerFacadeToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + var returnedCatalog = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(returnedCatalog); + } + + [TestMethod] + public void ConfigureContainerAddsRegionNavigationJournalEntryToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.BaseContainer.Resolve(); + var actual2 = bootstrapper.BaseContainer.Resolve(); + + Assert.IsNotNull(actual1); + Assert.IsNotNull(actual2); + Assert.AreNotSame(actual1, actual2); + } + + [TestMethod] + public void ConfigureContainerAddsRegionNavigationJournalToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.BaseContainer.Resolve(); + var actual2 = bootstrapper.BaseContainer.Resolve(); + + Assert.IsNotNull(actual1); + Assert.IsNotNull(actual2); + Assert.AreNotSame(actual1, actual2); + } + + [TestMethod] + public void ConfigureContainerAddsRegionNavigationServiceToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.BaseContainer.Resolve(); + var actual2 = bootstrapper.BaseContainer.Resolve(); + + Assert.IsNotNull(actual1); + Assert.IsNotNull(actual2); + Assert.AreNotSame(actual1, actual2); + } + + [TestMethod] + public void ConfigureContainerAddsNavigationTargetHandlerToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + var actual1 = bootstrapper.BaseContainer.Resolve(); + var actual2 = bootstrapper.BaseContainer.Resolve(); + + Assert.IsNotNull(actual1); + Assert.IsNotNull(actual2); + Assert.AreSame(actual1, actual2); + } + + [TestMethod] + public void RegisterFrameworkExceptionTypesShouldRegisterActivationException() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.CallRegisterFrameworkExceptionTypes(); + + Assert.IsTrue(ExceptionExtensions.IsFrameworkExceptionRegistered( + typeof(ContainerException))); + } + + [TestMethod] + public void RegisterFrameworkExceptionTypesShouldRegisterResolutionFailedException() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.CallRegisterFrameworkExceptionTypes(); + + Assert.IsTrue(ExceptionExtensions.IsFrameworkExceptionRegistered( + typeof(ContainerException))); + } + } + + internal class DefaultDryIocBootstrapper : DryIocBootstrapper + { + public List MethodCalls = new List(); + public bool InitializeModulesCalled; + public bool ConfigureRegionAdapterMappingsCalled; + public RegionAdapterMappings DefaultRegionAdapterMappings; + public bool CreateLoggerCalled; + public bool CreateModuleCatalogCalled; + public bool CreateShellCalled; + public bool ConfigureContainerCalled; + public bool CreateContainerCalled; + public bool ConfigureModuleCatalogCalled; + public bool InitializeShellCalled; + public bool ConfigureServiceLocatorCalled; + public bool ConfigureDefaultRegionBehaviorsCalled; + public IStyledProperty ShellObject = new UserControl(); + + public IStyledProperty BaseShell + { + get { return base.Shell; } + } + public IContainer BaseContainer + { + get { return base.Container; } + set { base.Container = value; } + } + + public MockLoggerAdapter BaseLogger + { + get { return base.Logger as MockLoggerAdapter; } + } + + public IContainer CallCreateContainer() + { + return CreateContainer(); + } + + protected override void ConfigureContainer() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + ConfigureContainerCalled = true; + base.ConfigureContainer(); + } + + protected override IContainer CreateContainer() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + CreateContainerCalled = true; + return base.CreateContainer(); + } + + protected override ILoggerFacade CreateLogger() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + CreateLoggerCalled = true; + return new MockLoggerAdapter(); + } + + protected override IStyledProperty CreateShell() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + CreateShellCalled = true; + return ShellObject; + } + + protected override void ConfigureServiceLocator() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + ConfigureServiceLocatorCalled = true; + base.ConfigureServiceLocator(); + } + protected override IModuleCatalog CreateModuleCatalog() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + CreateModuleCatalogCalled = true; + return base.CreateModuleCatalog(); + } + + protected override void ConfigureModuleCatalog() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + ConfigureModuleCatalogCalled = true; + base.ConfigureModuleCatalog(); + } + + protected override void InitializeShell() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + InitializeShellCalled = true; + // no op + } + + protected override void InitializeModules() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + InitializeModulesCalled = true; + base.InitializeModules(); + } + + protected override IRegionBehaviorFactory ConfigureDefaultRegionBehaviors() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + ConfigureDefaultRegionBehaviorsCalled = true; + return base.ConfigureDefaultRegionBehaviors(); + } + + protected override RegionAdapterMappings ConfigureRegionAdapterMappings() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + ConfigureRegionAdapterMappingsCalled = true; + var regionAdapterMappings = base.ConfigureRegionAdapterMappings(); + + DefaultRegionAdapterMappings = regionAdapterMappings; + + return regionAdapterMappings; + } + + protected override void RegisterFrameworkExceptionTypes() + { + MethodCalls.Add(MethodBase.GetCurrentMethod().Name); + base.RegisterFrameworkExceptionTypes(); + } + + public void CallRegisterFrameworkExceptionTypes() + { + base.RegisterFrameworkExceptionTypes(); + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullContainerFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullContainerFixture.cs new file mode 100644 index 0000000000..bafdf9e0b3 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullContainerFixture.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia; +using DryIoc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Prism.IocContainer.Avalonia.Tests.Support; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperNullContainerFixture : BootstrapperFixtureBase + { + [TestMethod] + public void RunThrowsWhenNullContainerCreated() + { + var bootstrapper = new NullContainerBootstrapper(); + + AssertExceptionThrownOnRun(bootstrapper, typeof(InvalidOperationException), "IContainer"); + } + + private class NullContainerBootstrapper : DryIocBootstrapper + { + protected override IContainer CreateContainer() + { + return null; + } + + protected override IStyledProperty CreateShell() + { + throw new NotImplementedException(); + } + + protected override void InitializeShell() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullLoggerFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullLoggerFixture.cs new file mode 100644 index 0000000000..098cea0efd --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullLoggerFixture.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Prism.IocContainer.Avalonia.Tests.Support; +using Prism.Logging; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperNullLoggerFixture : BootstrapperFixtureBase + { + [TestMethod] + public void NullLoggerThrows() + { + var bootstrapper = new NullLoggerBootstrapper(); + + AssertExceptionThrownOnRun(bootstrapper, typeof(InvalidOperationException), "ILoggerFacade"); + } + + internal class NullLoggerBootstrapper : DryIocBootstrapper + { + protected override ILoggerFacade CreateLogger() + { + return null; + } + + protected override IStyledProperty CreateShell() + { + throw new NotImplementedException(); + } + + protected override void InitializeShell() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleCatalogFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleCatalogFixture.cs new file mode 100644 index 0000000000..b19c3529b9 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleCatalogFixture.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Prism.IocContainer.Avalonia.Tests.Support; +using Prism.Modularity; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperNullModuleCatalogFixture : BootstrapperFixtureBase + { + [TestMethod] + public void NullModuleCatalogThrowsOnDefaultModuleInitialization() + { + var bootstrapper = new NullModuleCatalogBootstrapper(); + + AssertExceptionThrownOnRun(bootstrapper, typeof(InvalidOperationException), "IModuleCatalog"); + } + + private class NullModuleCatalogBootstrapper : DryIocBootstrapper + { + protected override IModuleCatalog CreateModuleCatalog() + { + return null; + } + + protected override IStyledProperty CreateShell() + { + throw new NotImplementedException(); + } + + protected override void InitializeShell() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleManagerFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleManagerFixture.cs new file mode 100644 index 0000000000..8c56d54ce3 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperNullModuleManagerFixture.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using Avalonia; +using DryIoc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; +using Prism.Logging; +using Prism.Regions; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperNullModuleManagerFixture + { + [TestMethod] + public void RunShouldNotCallInitializeModulesWhenModuleManagerNotFound() + { + var bootstrapper = new NullModuleManagerBootstrapper(); + + bootstrapper.Run(); + + Assert.IsFalse(bootstrapper.InitializeModulesCalled); + } + + private class NullModuleManagerBootstrapper : DryIocBootstrapper + { + public bool InitializeModulesCalled; + + protected override void ConfigureContainer() + { + Container.RegisterInstance(Logger); + Container.RegisterInstance(ModuleCatalog); + } + + protected override IRegionBehaviorFactory ConfigureDefaultRegionBehaviors() + { + return null; + } + + protected override RegionAdapterMappings ConfigureRegionAdapterMappings() + { + return null; + } + + protected override IStyledProperty CreateShell() + { + return null; + } + + protected override void InitializeModules() + { + this.InitializeModulesCalled = true; + } + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRegisterForNavigationFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRegisterForNavigationFixture.cs new file mode 100644 index 0000000000..90095f99d7 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRegisterForNavigationFixture.cs @@ -0,0 +1,37 @@ +using CommonServiceLocator; +using DryIoc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Prism.IocContainer.Avalonia.Tests.Support; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperRegisterForNavigationFixture : BootstrapperFixtureBase + { + [TestMethod] + public void RunCheckIfViewRegisteredForNavigationCanBeResolvedTroughServiceLocatorWithObjectServiceType() + { + var bootstrapper = new RegisterForNavigationBootstrapper(); + bootstrapper.Run(); + IServiceLocator serviceLocator = bootstrapper.Container.Resolve(); + object viewInstance = serviceLocator.GetInstance(nameof(NavigateView)); + + Assert.IsNotNull(viewInstance); + Assert.IsInstanceOfType(viewInstance, typeof(NavigateView)); + } + + private class RegisterForNavigationBootstrapper : DryIocBootstrapper + { + protected override void ConfigureContainer() + { + base.ConfigureContainer(); + Container.RegisterTypeForNavigation(); + } + } + + public class NavigateView + { + + } + } +} diff --git a/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRunMethodFixture.cs b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRunMethodFixture.cs new file mode 100644 index 0000000000..391a49d621 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/DryIocBootstrapperRunMethodFixture.cs @@ -0,0 +1,473 @@ +using System.Linq; +using CommonServiceLocator; +using DryIoc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Prism.Events; +using Prism.Logging; +using Prism.Modularity; +using Prism.Regions; + +namespace Prism.DryIoc.Avalonia.Tests +{ + [TestClass] + public class DryIocBootstrapperRunMethodFixture + { + [TestMethod] + public void CanRunBootstrapper() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + } + + [TestMethod] + public void RunShouldNotFailIfReturnedNullShell() + { + var bootstrapper = new DefaultDryIocBootstrapper { ShellObject = null }; + bootstrapper.Run(); + } + + [TestMethod] + public void RunConfiguresServiceLocatorProvider() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + Assert.IsTrue(ServiceLocator.Current is DryIocServiceLocatorAdapter); + } + + [TestMethod] + public void RunShouldInitializeContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + var container = bootstrapper.BaseContainer; + + Assert.IsNull(container); + + bootstrapper.Run(); + + container = bootstrapper.BaseContainer; + + Assert.IsNotNull(container); + Assert.IsInstanceOfType(container, typeof(IContainer)); + } + + [TestMethod] + public void RunAddsCompositionContainerToContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + var createdContainer = bootstrapper.CallCreateContainer(); + var returnedContainer = createdContainer.Resolve(); + Assert.IsNotNull(returnedContainer); + Assert.IsTrue(returnedContainer.GetType().GetInterfaces().Contains(typeof(IContainer))); + } + + [TestMethod] + public void RunShouldCallInitializeModules() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.InitializeModulesCalled); + } + + [TestMethod] + public void RunShouldCallConfigureDefaultRegionBehaviors() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.ConfigureDefaultRegionBehaviorsCalled); + } + + [TestMethod] + public void RunShouldCallConfigureRegionAdapterMappings() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.ConfigureRegionAdapterMappingsCalled); + } + + [TestMethod] + public void RunShouldAssignRegionManagerToReturnedShell() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsNotNull(RegionManager.GetRegionManager(bootstrapper.BaseShell)); + } + + [TestMethod] + public void RunShouldCallCreateLogger() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.CreateLoggerCalled); + } + + [TestMethod] + public void RunShouldCallCreateModuleCatalog() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.CreateModuleCatalogCalled); + } + + [TestMethod] + public void RunShouldCallConfigureModuleCatalog() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.ConfigureModuleCatalogCalled); + } + + [TestMethod] + public void RunShouldCallCreateContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.CreateContainerCalled); + } + + [TestMethod] + public void RunShouldCallCreateShell() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.CreateShellCalled); + } + + [TestMethod] + public void RunShouldCallConfigureContainer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + Assert.IsTrue(bootstrapper.ConfigureContainerCalled); + } + + // unable to mock extension RegisterInstance/RegisterType methods + // so registration is tested through checking the resolved type against interface + [TestMethod] + public void RunRegistersInstanceOfILoggerFacade() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var logger = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(logger); + Assert.IsTrue(logger.GetType().IsClass); + Assert.IsTrue(logger.GetType().GetInterfaces().Contains(typeof(ILoggerFacade))); + } + + [TestMethod] + public void RunRegistersInstanceOfIModuleCatalog() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var moduleCatalog = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(moduleCatalog); + Assert.IsTrue(moduleCatalog.GetType().IsClass); + Assert.IsTrue(moduleCatalog.GetType().GetInterfaces().Contains(typeof(IModuleCatalog))); + } + + [TestMethod] + public void RunRegistersTypeForIServiceLocator() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var serviceLocator = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(serviceLocator); + Assert.IsTrue(serviceLocator.GetType().IsClass); + Assert.AreEqual(typeof(DryIocServiceLocatorAdapter), serviceLocator.GetType()); + Assert.IsTrue(serviceLocator.GetType().GetInterfaces().Contains(typeof(IServiceLocator))); + } + + [TestMethod] + public void RunRegistersTypeForIModuleInitializer() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var moduleInitializer = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(moduleInitializer); + Assert.IsTrue(moduleInitializer.GetType().IsClass); + Assert.IsTrue(moduleInitializer.GetType().GetInterfaces().Contains(typeof(IModuleInitializer))); + } + + [TestMethod] + public void RunRegistersTypeForIRegionManager() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var regionManager = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(regionManager); + Assert.IsTrue(regionManager.GetType().IsClass); + Assert.IsTrue(regionManager.GetType().GetInterfaces().Contains(typeof(IRegionManager))); + } + + [TestMethod] + public void RunRegistersTypeForRegionAdapterMappings() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var regionAdapterMappings = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(regionAdapterMappings); + Assert.AreEqual(typeof(RegionAdapterMappings), regionAdapterMappings.GetType()); + } + + [TestMethod] + public void RunRegistersTypeForIRegionViewRegistry() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var regionViewRegistry = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(regionViewRegistry); + Assert.IsTrue(regionViewRegistry.GetType().IsClass); + Assert.IsTrue(regionViewRegistry.GetType().GetInterfaces().Contains(typeof(IRegionViewRegistry))); + } + + [TestMethod] + public void RunRegistersTypeForIRegionBehaviorFactory() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var regionBehaviorFactory = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(regionBehaviorFactory); + Assert.IsTrue(regionBehaviorFactory.GetType().IsClass); + Assert.IsTrue(regionBehaviorFactory.GetType().GetInterfaces().Contains(typeof(IRegionBehaviorFactory))); + } + + [TestMethod] + public void RunRegistersTypeForIEventAggregator() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + + var eventAggregator = bootstrapper.BaseContainer.Resolve(); + Assert.IsNotNull(eventAggregator); + Assert.IsTrue(eventAggregator.GetType().IsClass); + Assert.IsTrue(eventAggregator.GetType().GetInterfaces().Contains(typeof(IEventAggregator))); + } + + [TestMethod] + public void RunShouldCallTheMethodsInOrder() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + + Assert.AreEqual("CreateLogger", bootstrapper.MethodCalls[0]); + Assert.AreEqual("CreateModuleCatalog", bootstrapper.MethodCalls[1]); + Assert.AreEqual("ConfigureModuleCatalog", bootstrapper.MethodCalls[2]); + Assert.AreEqual("CreateContainer", bootstrapper.MethodCalls[3]); + Assert.AreEqual("ConfigureContainer", bootstrapper.MethodCalls[4]); + Assert.AreEqual("ConfigureServiceLocator", bootstrapper.MethodCalls[5]); + Assert.AreEqual("ConfigureRegionAdapterMappings", bootstrapper.MethodCalls[6]); + Assert.AreEqual("ConfigureDefaultRegionBehaviors", bootstrapper.MethodCalls[7]); + Assert.AreEqual("RegisterFrameworkExceptionTypes", bootstrapper.MethodCalls[8]); + Assert.AreEqual("CreateShell", bootstrapper.MethodCalls[9]); + Assert.AreEqual("InitializeShell", bootstrapper.MethodCalls[10]); + Assert.AreEqual("InitializeModules", bootstrapper.MethodCalls[11]); + } + + [TestMethod] + public void RunShouldLogBootstrapperSteps() + { + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages[0].Contains("Logger was created successfully.")); + Assert.IsTrue(messages[1].Contains("Creating module catalog.")); + Assert.IsTrue(messages[2].Contains("Configuring module catalog.")); + Assert.IsTrue(messages[3].Contains("Creating DryIoc container.")); + Assert.IsTrue(messages[4].Contains("Configuring the DryIoc container.")); + Assert.IsTrue(messages[5].Contains("Configuring ServiceLocator singleton.")); + Assert.IsTrue(messages[6].Contains("Configuring the ViewModelLocator to use DryIoc.")); + Assert.IsTrue(messages[7].Contains("Configuring region adapters.")); + Assert.IsTrue(messages[8].Contains("Configuring default region behaviors.")); + Assert.IsTrue(messages[9].Contains("Registering Framework Exception Types.")); + Assert.IsTrue(messages[10].Contains("Creating the shell.")); + Assert.IsTrue(messages[11].Contains("Setting the RegionManager.")); + Assert.IsTrue(messages[12].Contains("Updating Regions.")); + Assert.IsTrue(messages[13].Contains("Initializing the shell.")); + Assert.IsTrue(messages[14].Contains("Initializing modules.")); + Assert.IsTrue(messages[15].Contains("Bootstrapper sequence completed.")); + } + + [TestMethod] + public void RunShouldLogLoggerCreationSuccess() + { + const string expectedMessageText = "Logger was created successfully."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + [TestMethod] + public void RunShouldLogAboutModuleCatalogCreation() + { + const string expectedMessageText = "Creating module catalog."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutConfiguringModuleCatalog() + { + const string expectedMessageText = "Configuring module catalog."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutCreatingTheContainer() + { + const string expectedMessageText = "Creating DryIoc container."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutConfiguringContainerBuilder() + { + const string expectedMessageText = "Configuring the DryIoc container."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutConfiguringRegionAdapters() + { + const string expectedMessageText = "Configuring region adapters."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + + [TestMethod] + public void RunShouldLogAboutConfiguringRegionBehaviors() + { + const string expectedMessageText = "Configuring default region behaviors."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutRegisteringFrameworkExceptionTypes() + { + const string expectedMessageText = "Registering Framework Exception Types."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutCreatingTheShell() + { + const string expectedMessageText = "Creating the shell."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutInitializingTheShellIfShellCreated() + { + const string expectedMessageText = "Initializing the shell."; + var bootstrapper = new DefaultDryIocBootstrapper(); + + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldNotLogAboutInitializingTheShellIfShellIsNotCreated() + { + const string expectedMessageText = "Initializing shell"; + var bootstrapper = new DefaultDryIocBootstrapper { ShellObject = null }; + + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsFalse(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutInitializingModules() + { + const string expectedMessageText = "Initializing modules."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + + [TestMethod] + public void RunShouldLogAboutRunCompleting() + { + const string expectedMessageText = "Bootstrapper sequence completed."; + var bootstrapper = new DefaultDryIocBootstrapper(); + bootstrapper.Run(); + var messages = bootstrapper.BaseLogger.Messages; + + Assert.IsTrue(messages.Contains(expectedMessageText)); + } + } +} 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..0820edab66 --- /dev/null +++ b/tests/Avalonia/Prism.DryIoc.Avalonia.Tests/Prism.DryIoc.Avalonia.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + +