From 8393aaac9e8aacc178c9bfe49724284b48150bc6 Mon Sep 17 00:00:00 2001 From: mr5z Date: Tue, 30 Jun 2026 03:31:40 +0800 Subject: [PATCH] WIP --- MvvmEssentials.sln | 6 + Source/Attributes/ParameterAttribute.cs | 6 - {Source => src}/AssemblyInfo.cs | 0 src/Attributes/ParameterAttribute.cs | 9 ++ .../FlyoutDetailLifecycleBehavior.cs | 0 .../Behaviors/FlyoutPresentingBehavior.cs | 0 .../Behaviors/TabSelectionBehavior.cs | 0 .../Extensions/ContentViewFactoryExtension.cs | 0 .../Extensions/MauiAppBuilderExtension.cs | 0 .../NavigationServiceCollectionExtension.cs | 0 .../Extensions/NavigationServiceExtension.cs | 14 ++ .../PageRegistryCollectionExtension.cs | 0 .../Extensions/PopupServiceExtension.cs | 0 {Source => src}/Helpers/PageHelper.cs | 0 {Source => src}/IsExternalInit.cs | 2 +- {Source => src}/Nkraft.MvvmEssentials.csproj | 8 +- {Source => src}/Nkraft.MvvmEssentials.targets | 0 {Source => src}/Pages/PopupPage.cs | 0 {Source => src}/Services/AppStartup.cs | 0 .../Services/ApplicationContext.cs | 0 .../ContentViews/ContentViewFactory.cs | 0 .../Services/FlyoutPages/FlyoutComponent.cs | 0 .../Services/FlyoutPages/FlyoutHost.cs | 0 .../Services/Handlers/FlyoutPageHandler.cs | 0 .../Handlers/NavigationPageHandler.cs | 0 .../Services/Handlers/TabbedPageHandler.cs | 0 .../Handlers/UnsupportedPageHandler.cs | 0 .../Services/Helpers/ExceptionDispatcher.cs | 0 .../Services/Navigation/NavigationAware.cs | 0 .../Services/Navigation/NavigationHelper.cs | 0 .../Services/Navigation/NavigationHints.cs | 0 .../Navigation/NavigationParameters.cs | 0 {Source => src}/Services/NavigationService.cs | 0 .../Services/Pages/PageAppearingAware.cs | 0 {Source => src}/Services/Pages/PageFactory.cs | 0 {Source => src}/Services/Pages/PageInfo.cs | 0 {Source => src}/Services/Pages/PageLink.cs | 0 .../Services/Pages/PageLoadAware.cs | 0 .../Services/Pages/PageNavigationHandler.cs | 0 .../Services/Pages/PageRegistry.cs | 0 .../Services/Pages/ParameterSetAware.cs | 0 {Source => src}/Services/PopupService.cs | 0 .../Services/Popups/PopupDismissible.cs | 0 .../Services/TabbedPages/TabComponent.cs | 0 .../Services/TabbedPages/TabHost.cs | 0 .../Services/Wizards/WizardStep.cs | 0 .../AnalyzerReleases.Shipped.md | 0 .../AnalyzerReleases.Unshipped.md | 0 .../SourceGenerator/AppStartupGenerator.cs | 0 .../SourceGenerator/Diagnostics.cs | 0 .../InitialViewModelDetector.cs | 0 src/SourceGenerator/IsExternalInit.cs | 7 + .../NavigationParametersGenerator.cs | 152 ++++++++++++++++++ ...raft.MvvmEssentials.SourceGenerator.csproj | 0 {Source => src}/ViewModels/BaseViewModel.cs | 0 .../ViewModels/FlyoutHostViewModel.cs | 0 .../ViewModels/FlyoutMenuViewModel.cs | 0 .../ViewModels/NavigableEntryViewModel.cs | 7 +- {Source => src}/ViewModels/PageViewModel.cs | 0 {Source => src}/ViewModels/PopupViewModel.cs | 0 .../ViewModels/TabHostViewModel.cs | 4 +- {Source => src}/ViewModels/TabViewModel.cs | 0 .../ViewModels/WizardHostViewModel.cs | 0 .../ViewModels/WizardStepViewModel.cs | 0 .../NavigationServiceExtensionTest.cs | 0 {Tests => tests}/Fakes/Fakes.cs | 0 {Tests => tests}/Helpers/PageHelperTests.cs | 0 .../Nkraft.MvvmEssentials.UnitTest.csproj | 2 + .../Services/AppStartupWindowHookTests.cs | 0 .../Navigation/NavigationParametersTests.cs | 0 .../Services/Navigation/PageLinkTests.cs | 0 .../Services/Navigation/PageRegistryTests.cs | 0 .../Services/NavigationServiceTests.cs | 0 .../ViewModels/BaseViewModelTests.cs | 0 .../ViewModels/FlyoutMenuViewModelTests.cs | 0 .../NavigableEntryViewModelTests.cs | 0 .../NavigationParametersGeneratorTests.cs | 58 +++++++ .../ViewModels/PageViewModelTests.cs | 0 .../ViewModels/PopupViewModelTests.cs | 0 .../ViewModels/TabHostViewModelTests.cs | 0 .../ViewModels/TabViewModelTests.cs | 0 81 files changed, 264 insertions(+), 11 deletions(-) delete mode 100644 Source/Attributes/ParameterAttribute.cs rename {Source => src}/AssemblyInfo.cs (100%) create mode 100644 src/Attributes/ParameterAttribute.cs rename {Source => src}/Behaviors/FlyoutDetailLifecycleBehavior.cs (100%) rename {Source => src}/Behaviors/FlyoutPresentingBehavior.cs (100%) rename {Source => src}/Behaviors/TabSelectionBehavior.cs (100%) rename {Source => src}/Extensions/ContentViewFactoryExtension.cs (100%) rename {Source => src}/Extensions/MauiAppBuilderExtension.cs (100%) rename {Source => src}/Extensions/NavigationServiceCollectionExtension.cs (100%) rename {Source => src}/Extensions/NavigationServiceExtension.cs (93%) rename {Source => src}/Extensions/PageRegistryCollectionExtension.cs (100%) rename {Source => src}/Extensions/PopupServiceExtension.cs (100%) rename {Source => src}/Helpers/PageHelper.cs (100%) rename {Source => src}/IsExternalInit.cs (67%) rename {Source => src}/Nkraft.MvvmEssentials.csproj (87%) rename {Source => src}/Nkraft.MvvmEssentials.targets (100%) rename {Source => src}/Pages/PopupPage.cs (100%) rename {Source => src}/Services/AppStartup.cs (100%) rename {Source => src}/Services/ApplicationContext.cs (100%) rename {Source => src}/Services/ContentViews/ContentViewFactory.cs (100%) rename {Source => src}/Services/FlyoutPages/FlyoutComponent.cs (100%) rename {Source => src}/Services/FlyoutPages/FlyoutHost.cs (100%) rename {Source => src}/Services/Handlers/FlyoutPageHandler.cs (100%) rename {Source => src}/Services/Handlers/NavigationPageHandler.cs (100%) rename {Source => src}/Services/Handlers/TabbedPageHandler.cs (100%) rename {Source => src}/Services/Handlers/UnsupportedPageHandler.cs (100%) rename {Source => src}/Services/Helpers/ExceptionDispatcher.cs (100%) rename {Source => src}/Services/Navigation/NavigationAware.cs (100%) rename {Source => src}/Services/Navigation/NavigationHelper.cs (100%) rename {Source => src}/Services/Navigation/NavigationHints.cs (100%) rename {Source => src}/Services/Navigation/NavigationParameters.cs (100%) rename {Source => src}/Services/NavigationService.cs (100%) rename {Source => src}/Services/Pages/PageAppearingAware.cs (100%) rename {Source => src}/Services/Pages/PageFactory.cs (100%) rename {Source => src}/Services/Pages/PageInfo.cs (100%) rename {Source => src}/Services/Pages/PageLink.cs (100%) rename {Source => src}/Services/Pages/PageLoadAware.cs (100%) rename {Source => src}/Services/Pages/PageNavigationHandler.cs (100%) rename {Source => src}/Services/Pages/PageRegistry.cs (100%) rename {Source => src}/Services/Pages/ParameterSetAware.cs (100%) rename {Source => src}/Services/PopupService.cs (100%) rename {Source => src}/Services/Popups/PopupDismissible.cs (100%) rename {Source => src}/Services/TabbedPages/TabComponent.cs (100%) rename {Source => src}/Services/TabbedPages/TabHost.cs (100%) rename {Source => src}/Services/Wizards/WizardStep.cs (100%) rename {Source => src}/SourceGenerator/AnalyzerReleases.Shipped.md (100%) rename {Source => src}/SourceGenerator/AnalyzerReleases.Unshipped.md (100%) rename {Source => src}/SourceGenerator/AppStartupGenerator.cs (100%) rename {Source => src}/SourceGenerator/Diagnostics.cs (100%) rename {Source => src}/SourceGenerator/InitialViewModelDetector.cs (100%) create mode 100644 src/SourceGenerator/IsExternalInit.cs create mode 100644 src/SourceGenerator/NavigationParametersGenerator.cs rename {Source => src}/SourceGenerator/Nkraft.MvvmEssentials.SourceGenerator.csproj (100%) rename {Source => src}/ViewModels/BaseViewModel.cs (100%) rename {Source => src}/ViewModels/FlyoutHostViewModel.cs (100%) rename {Source => src}/ViewModels/FlyoutMenuViewModel.cs (100%) rename {Source => src}/ViewModels/NavigableEntryViewModel.cs (80%) rename {Source => src}/ViewModels/PageViewModel.cs (100%) rename {Source => src}/ViewModels/PopupViewModel.cs (100%) rename {Source => src}/ViewModels/TabHostViewModel.cs (92%) rename {Source => src}/ViewModels/TabViewModel.cs (100%) rename {Source => src}/ViewModels/WizardHostViewModel.cs (100%) rename {Source => src}/ViewModels/WizardStepViewModel.cs (100%) rename {Tests => tests}/Extensions/NavigationServiceExtensionTest.cs (100%) rename {Tests => tests}/Fakes/Fakes.cs (100%) rename {Tests => tests}/Helpers/PageHelperTests.cs (100%) rename {Tests => tests}/Nkraft.MvvmEssentials.UnitTest.csproj (78%) rename {Tests => tests}/Services/AppStartupWindowHookTests.cs (100%) rename {Tests => tests}/Services/Navigation/NavigationParametersTests.cs (100%) rename {Tests => tests}/Services/Navigation/PageLinkTests.cs (100%) rename {Tests => tests}/Services/Navigation/PageRegistryTests.cs (100%) rename {Tests => tests}/Services/NavigationServiceTests.cs (100%) rename {Tests => tests}/ViewModels/BaseViewModelTests.cs (100%) rename {Tests => tests}/ViewModels/FlyoutMenuViewModelTests.cs (100%) rename {Tests => tests}/ViewModels/NavigableEntryViewModelTests.cs (100%) create mode 100644 tests/ViewModels/NavigationParametersGeneratorTests.cs rename {Tests => tests}/ViewModels/PageViewModelTests.cs (100%) rename {Tests => tests}/ViewModels/PopupViewModelTests.cs (100%) rename {Tests => tests}/ViewModels/TabHostViewModelTests.cs (100%) rename {Tests => tests}/ViewModels/TabViewModelTests.cs (100%) diff --git a/MvvmEssentials.sln b/MvvmEssentials.sln index c85fdc7..a46879e 100644 --- a/MvvmEssentials.sln +++ b/MvvmEssentials.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nkraft.CrossUtility", "..\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nkraft.MvvmEssentials.UnitTest", "Tests\Nkraft.MvvmEssentials.UnitTest.csproj", "{E4346076-4B31-4966-BDF1-7653B584296F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FoodDelivery", "..\FoodDelivery\FoodDelivery\FoodDelivery.csproj", "{73066B82-45E7-4AD0-AD36-852A369F6E77}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {E4346076-4B31-4966-BDF1-7653B584296F}.Debug|Any CPU.Build.0 = Debug|Any CPU {E4346076-4B31-4966-BDF1-7653B584296F}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4346076-4B31-4966-BDF1-7653B584296F}.Release|Any CPU.Build.0 = Release|Any CPU + {73066B82-45E7-4AD0-AD36-852A369F6E77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73066B82-45E7-4AD0-AD36-852A369F6E77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73066B82-45E7-4AD0-AD36-852A369F6E77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73066B82-45E7-4AD0-AD36-852A369F6E77}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/Attributes/ParameterAttribute.cs b/Source/Attributes/ParameterAttribute.cs deleted file mode 100644 index d00ff2b..0000000 --- a/Source/Attributes/ParameterAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace System.Runtime.CompilerServices.Attributes; - -public class ParameterAttribute -{ - -} \ No newline at end of file diff --git a/Source/AssemblyInfo.cs b/src/AssemblyInfo.cs similarity index 100% rename from Source/AssemblyInfo.cs rename to src/AssemblyInfo.cs diff --git a/src/Attributes/ParameterAttribute.cs b/src/Attributes/ParameterAttribute.cs new file mode 100644 index 0000000..fc23018 --- /dev/null +++ b/src/Attributes/ParameterAttribute.cs @@ -0,0 +1,9 @@ +namespace Nkraft.MvvmEssentials.Attributes; + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class ParameterAttribute : Attribute +{ + public bool IsRequired { get; set; } + + public string? PreferredPropertyName { get; set; } +} diff --git a/Source/Behaviors/FlyoutDetailLifecycleBehavior.cs b/src/Behaviors/FlyoutDetailLifecycleBehavior.cs similarity index 100% rename from Source/Behaviors/FlyoutDetailLifecycleBehavior.cs rename to src/Behaviors/FlyoutDetailLifecycleBehavior.cs diff --git a/Source/Behaviors/FlyoutPresentingBehavior.cs b/src/Behaviors/FlyoutPresentingBehavior.cs similarity index 100% rename from Source/Behaviors/FlyoutPresentingBehavior.cs rename to src/Behaviors/FlyoutPresentingBehavior.cs diff --git a/Source/Behaviors/TabSelectionBehavior.cs b/src/Behaviors/TabSelectionBehavior.cs similarity index 100% rename from Source/Behaviors/TabSelectionBehavior.cs rename to src/Behaviors/TabSelectionBehavior.cs diff --git a/Source/Extensions/ContentViewFactoryExtension.cs b/src/Extensions/ContentViewFactoryExtension.cs similarity index 100% rename from Source/Extensions/ContentViewFactoryExtension.cs rename to src/Extensions/ContentViewFactoryExtension.cs diff --git a/Source/Extensions/MauiAppBuilderExtension.cs b/src/Extensions/MauiAppBuilderExtension.cs similarity index 100% rename from Source/Extensions/MauiAppBuilderExtension.cs rename to src/Extensions/MauiAppBuilderExtension.cs diff --git a/Source/Extensions/NavigationServiceCollectionExtension.cs b/src/Extensions/NavigationServiceCollectionExtension.cs similarity index 100% rename from Source/Extensions/NavigationServiceCollectionExtension.cs rename to src/Extensions/NavigationServiceCollectionExtension.cs diff --git a/Source/Extensions/NavigationServiceExtension.cs b/src/Extensions/NavigationServiceExtension.cs similarity index 93% rename from Source/Extensions/NavigationServiceExtension.cs rename to src/Extensions/NavigationServiceExtension.cs index 288421b..2bea001 100644 --- a/Source/Extensions/NavigationServiceExtension.cs +++ b/src/Extensions/NavigationServiceExtension.cs @@ -8,10 +8,24 @@ // ReSharper disable once CheckNamespace namespace Nkraft.MvvmEssentials; +public interface INavigationTarget; + public static class NavigationExtension { extension(INavigationService navigationService) { + public async Task NavigateAsync( + INavigationTarget parameters, bool animated = true) + where TViewModel : PageViewModel + { + var pageName = PageHelper.ToPageName("Page"); + var navParam = new NavigationParameters(); + foreach (var (key, value) in ObjectHelper.ToDictionary(parameters)) + navParam.Add(key, value); + return await navigationService.NavigateAsync(pageName, navParam, animated); + } + + /// /// Navigates to the page associated with the specified ViewModel type, passing parameters as a strongly-typed object. /// diff --git a/Source/Extensions/PageRegistryCollectionExtension.cs b/src/Extensions/PageRegistryCollectionExtension.cs similarity index 100% rename from Source/Extensions/PageRegistryCollectionExtension.cs rename to src/Extensions/PageRegistryCollectionExtension.cs diff --git a/Source/Extensions/PopupServiceExtension.cs b/src/Extensions/PopupServiceExtension.cs similarity index 100% rename from Source/Extensions/PopupServiceExtension.cs rename to src/Extensions/PopupServiceExtension.cs diff --git a/Source/Helpers/PageHelper.cs b/src/Helpers/PageHelper.cs similarity index 100% rename from Source/Helpers/PageHelper.cs rename to src/Helpers/PageHelper.cs diff --git a/Source/IsExternalInit.cs b/src/IsExternalInit.cs similarity index 67% rename from Source/IsExternalInit.cs rename to src/IsExternalInit.cs index 582e8aa..8a53989 100644 --- a/Source/IsExternalInit.cs +++ b/src/IsExternalInit.cs @@ -1,4 +1,4 @@ -// IsExternalInit.cs +// ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices; internal static class IsExternalInit { } \ No newline at end of file diff --git a/Source/Nkraft.MvvmEssentials.csproj b/src/Nkraft.MvvmEssentials.csproj similarity index 87% rename from Source/Nkraft.MvvmEssentials.csproj rename to src/Nkraft.MvvmEssentials.csproj index 1a026e9..794b430 100644 --- a/Source/Nkraft.MvvmEssentials.csproj +++ b/src/Nkraft.MvvmEssentials.csproj @@ -73,8 +73,14 @@ + PrivateAssets="all" + Condition="'$(Configuration)' == 'Debug'" /> + diff --git a/Source/Nkraft.MvvmEssentials.targets b/src/Nkraft.MvvmEssentials.targets similarity index 100% rename from Source/Nkraft.MvvmEssentials.targets rename to src/Nkraft.MvvmEssentials.targets diff --git a/Source/Pages/PopupPage.cs b/src/Pages/PopupPage.cs similarity index 100% rename from Source/Pages/PopupPage.cs rename to src/Pages/PopupPage.cs diff --git a/Source/Services/AppStartup.cs b/src/Services/AppStartup.cs similarity index 100% rename from Source/Services/AppStartup.cs rename to src/Services/AppStartup.cs diff --git a/Source/Services/ApplicationContext.cs b/src/Services/ApplicationContext.cs similarity index 100% rename from Source/Services/ApplicationContext.cs rename to src/Services/ApplicationContext.cs diff --git a/Source/Services/ContentViews/ContentViewFactory.cs b/src/Services/ContentViews/ContentViewFactory.cs similarity index 100% rename from Source/Services/ContentViews/ContentViewFactory.cs rename to src/Services/ContentViews/ContentViewFactory.cs diff --git a/Source/Services/FlyoutPages/FlyoutComponent.cs b/src/Services/FlyoutPages/FlyoutComponent.cs similarity index 100% rename from Source/Services/FlyoutPages/FlyoutComponent.cs rename to src/Services/FlyoutPages/FlyoutComponent.cs diff --git a/Source/Services/FlyoutPages/FlyoutHost.cs b/src/Services/FlyoutPages/FlyoutHost.cs similarity index 100% rename from Source/Services/FlyoutPages/FlyoutHost.cs rename to src/Services/FlyoutPages/FlyoutHost.cs diff --git a/Source/Services/Handlers/FlyoutPageHandler.cs b/src/Services/Handlers/FlyoutPageHandler.cs similarity index 100% rename from Source/Services/Handlers/FlyoutPageHandler.cs rename to src/Services/Handlers/FlyoutPageHandler.cs diff --git a/Source/Services/Handlers/NavigationPageHandler.cs b/src/Services/Handlers/NavigationPageHandler.cs similarity index 100% rename from Source/Services/Handlers/NavigationPageHandler.cs rename to src/Services/Handlers/NavigationPageHandler.cs diff --git a/Source/Services/Handlers/TabbedPageHandler.cs b/src/Services/Handlers/TabbedPageHandler.cs similarity index 100% rename from Source/Services/Handlers/TabbedPageHandler.cs rename to src/Services/Handlers/TabbedPageHandler.cs diff --git a/Source/Services/Handlers/UnsupportedPageHandler.cs b/src/Services/Handlers/UnsupportedPageHandler.cs similarity index 100% rename from Source/Services/Handlers/UnsupportedPageHandler.cs rename to src/Services/Handlers/UnsupportedPageHandler.cs diff --git a/Source/Services/Helpers/ExceptionDispatcher.cs b/src/Services/Helpers/ExceptionDispatcher.cs similarity index 100% rename from Source/Services/Helpers/ExceptionDispatcher.cs rename to src/Services/Helpers/ExceptionDispatcher.cs diff --git a/Source/Services/Navigation/NavigationAware.cs b/src/Services/Navigation/NavigationAware.cs similarity index 100% rename from Source/Services/Navigation/NavigationAware.cs rename to src/Services/Navigation/NavigationAware.cs diff --git a/Source/Services/Navigation/NavigationHelper.cs b/src/Services/Navigation/NavigationHelper.cs similarity index 100% rename from Source/Services/Navigation/NavigationHelper.cs rename to src/Services/Navigation/NavigationHelper.cs diff --git a/Source/Services/Navigation/NavigationHints.cs b/src/Services/Navigation/NavigationHints.cs similarity index 100% rename from Source/Services/Navigation/NavigationHints.cs rename to src/Services/Navigation/NavigationHints.cs diff --git a/Source/Services/Navigation/NavigationParameters.cs b/src/Services/Navigation/NavigationParameters.cs similarity index 100% rename from Source/Services/Navigation/NavigationParameters.cs rename to src/Services/Navigation/NavigationParameters.cs diff --git a/Source/Services/NavigationService.cs b/src/Services/NavigationService.cs similarity index 100% rename from Source/Services/NavigationService.cs rename to src/Services/NavigationService.cs diff --git a/Source/Services/Pages/PageAppearingAware.cs b/src/Services/Pages/PageAppearingAware.cs similarity index 100% rename from Source/Services/Pages/PageAppearingAware.cs rename to src/Services/Pages/PageAppearingAware.cs diff --git a/Source/Services/Pages/PageFactory.cs b/src/Services/Pages/PageFactory.cs similarity index 100% rename from Source/Services/Pages/PageFactory.cs rename to src/Services/Pages/PageFactory.cs diff --git a/Source/Services/Pages/PageInfo.cs b/src/Services/Pages/PageInfo.cs similarity index 100% rename from Source/Services/Pages/PageInfo.cs rename to src/Services/Pages/PageInfo.cs diff --git a/Source/Services/Pages/PageLink.cs b/src/Services/Pages/PageLink.cs similarity index 100% rename from Source/Services/Pages/PageLink.cs rename to src/Services/Pages/PageLink.cs diff --git a/Source/Services/Pages/PageLoadAware.cs b/src/Services/Pages/PageLoadAware.cs similarity index 100% rename from Source/Services/Pages/PageLoadAware.cs rename to src/Services/Pages/PageLoadAware.cs diff --git a/Source/Services/Pages/PageNavigationHandler.cs b/src/Services/Pages/PageNavigationHandler.cs similarity index 100% rename from Source/Services/Pages/PageNavigationHandler.cs rename to src/Services/Pages/PageNavigationHandler.cs diff --git a/Source/Services/Pages/PageRegistry.cs b/src/Services/Pages/PageRegistry.cs similarity index 100% rename from Source/Services/Pages/PageRegistry.cs rename to src/Services/Pages/PageRegistry.cs diff --git a/Source/Services/Pages/ParameterSetAware.cs b/src/Services/Pages/ParameterSetAware.cs similarity index 100% rename from Source/Services/Pages/ParameterSetAware.cs rename to src/Services/Pages/ParameterSetAware.cs diff --git a/Source/Services/PopupService.cs b/src/Services/PopupService.cs similarity index 100% rename from Source/Services/PopupService.cs rename to src/Services/PopupService.cs diff --git a/Source/Services/Popups/PopupDismissible.cs b/src/Services/Popups/PopupDismissible.cs similarity index 100% rename from Source/Services/Popups/PopupDismissible.cs rename to src/Services/Popups/PopupDismissible.cs diff --git a/Source/Services/TabbedPages/TabComponent.cs b/src/Services/TabbedPages/TabComponent.cs similarity index 100% rename from Source/Services/TabbedPages/TabComponent.cs rename to src/Services/TabbedPages/TabComponent.cs diff --git a/Source/Services/TabbedPages/TabHost.cs b/src/Services/TabbedPages/TabHost.cs similarity index 100% rename from Source/Services/TabbedPages/TabHost.cs rename to src/Services/TabbedPages/TabHost.cs diff --git a/Source/Services/Wizards/WizardStep.cs b/src/Services/Wizards/WizardStep.cs similarity index 100% rename from Source/Services/Wizards/WizardStep.cs rename to src/Services/Wizards/WizardStep.cs diff --git a/Source/SourceGenerator/AnalyzerReleases.Shipped.md b/src/SourceGenerator/AnalyzerReleases.Shipped.md similarity index 100% rename from Source/SourceGenerator/AnalyzerReleases.Shipped.md rename to src/SourceGenerator/AnalyzerReleases.Shipped.md diff --git a/Source/SourceGenerator/AnalyzerReleases.Unshipped.md b/src/SourceGenerator/AnalyzerReleases.Unshipped.md similarity index 100% rename from Source/SourceGenerator/AnalyzerReleases.Unshipped.md rename to src/SourceGenerator/AnalyzerReleases.Unshipped.md diff --git a/Source/SourceGenerator/AppStartupGenerator.cs b/src/SourceGenerator/AppStartupGenerator.cs similarity index 100% rename from Source/SourceGenerator/AppStartupGenerator.cs rename to src/SourceGenerator/AppStartupGenerator.cs diff --git a/Source/SourceGenerator/Diagnostics.cs b/src/SourceGenerator/Diagnostics.cs similarity index 100% rename from Source/SourceGenerator/Diagnostics.cs rename to src/SourceGenerator/Diagnostics.cs diff --git a/Source/SourceGenerator/InitialViewModelDetector.cs b/src/SourceGenerator/InitialViewModelDetector.cs similarity index 100% rename from Source/SourceGenerator/InitialViewModelDetector.cs rename to src/SourceGenerator/InitialViewModelDetector.cs diff --git a/src/SourceGenerator/IsExternalInit.cs b/src/SourceGenerator/IsExternalInit.cs new file mode 100644 index 0000000..616651e --- /dev/null +++ b/src/SourceGenerator/IsExternalInit.cs @@ -0,0 +1,7 @@ +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit { } diff --git a/src/SourceGenerator/NavigationParametersGenerator.cs b/src/SourceGenerator/NavigationParametersGenerator.cs new file mode 100644 index 0000000..6d4828f --- /dev/null +++ b/src/SourceGenerator/NavigationParametersGenerator.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Nkraft.MvvmEssentials.SourceGenerator; + +// per-property (your type, now a record so it has value equality for the incremental cache) +public sealed record ViewModelParameters(string PropertyName, string PropertyType, bool IsRequired); + +// per-VM — this is what was missing: carries where to emit +internal sealed record ViewModelTarget(string Namespace, string ClassName, EquatableArray Parameters); + +[Generator] +public class NavigationParametersGenerator : IIncrementalGenerator +{ + private const string NavigableViewModelName = "Nkraft.MvvmEssentials.ViewModels.NavigableEntryViewModel"; + private const string NavigationParameterName = "Nkraft.MvvmEssentials.Attributes.ParameterAttribute"; + + void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context) + { + var targets = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => ClassPropertiesHasAttributes(node), + transform: static (ctx, _) => ToViewModelTarget(ctx)) + .Where(static t => t is not null) + .Select(static (t, _) => t!); + + context.RegisterSourceOutput(targets, static (spc, t) => Emit(spc, t)); + } + + private static bool ClassPropertiesHasAttributes(SyntaxNode node) => + node is ClassDeclarationSyntax cls && + cls.Members.OfType().Any(p => p.AttributeLists.Count > 0); + + private static ViewModelTarget? ToViewModelTarget(GeneratorSyntaxContext ctx) + { + var classDecl = (ClassDeclarationSyntax)ctx.Node; + if (ctx.SemanticModel.GetDeclaredSymbol(classDecl) is not INamedTypeSymbol symbol) + return null; + if (IsDerivedFrom(symbol, NavigableViewModelName) == false) + return null; + + var props = symbol.GetMembers() + .OfType() + .Where(p => p.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == NavigationParameterName)) + .Select(p => + { + var attr = p.GetAttributes().First(a => a.AttributeClass?.ToDisplayString() == NavigationParameterName); + // absent → required by default; opt out with [Parameter(IsRequired = false)] + var isRequired = attr.NamedArguments.FirstOrDefault(na => na.Key == "IsRequired").Value.Value as bool? ?? true; + return new ViewModelParameters( + p.Name, + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), // global:: → no resolution surprises + isRequired); + }) + .ToArray(); + + if (props.Length == 0) + return null; + + var ns = symbol.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : symbol.ContainingNamespace.ToDisplayString(); + + return new ViewModelTarget(ns, symbol.Name, new EquatableArray(props)); + } + + private static void Emit(SourceProductionContext spc, ViewModelTarget t) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + if (t.Namespace.Length > 0) + sb.Append("namespace ").Append(t.Namespace).AppendLine(";").AppendLine(); + + sb.Append("partial class ").AppendLine(t.ClassName); + sb.AppendLine("{"); + + sb.Append(" public sealed record Parameters : global::Nkraft.MvvmEssentials.INavigationTarget<") + .Append(t.ClassName).AppendLine(">"); + sb.AppendLine(" {"); + foreach (var p in t.Parameters.Array) + { + sb.Append(" public "); + if (p.IsRequired) sb.Append("required "); + sb.Append(p.PropertyType).Append(' ').Append(p.PropertyName).AppendLine(" { get; init; }"); + } + sb.AppendLine(); + sb.AppendLine(" public global::Nkraft.MvvmEssentials.Services.INavigationParameters ToNavigationParameters()"); + sb.AppendLine(" {"); + sb.AppendLine(" var __p = new global::Nkraft.MvvmEssentials.Services.NavigationParameters();"); + foreach (var p in t.Parameters.Array) + sb.Append(" __p.Add(\"").Append(p.PropertyName).Append("\", ").Append(p.PropertyName).AppendLine(");"); + sb.AppendLine(" return __p;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(); + + sb.AppendLine(" protected internal override void ApplyNavigationParameters(global::Nkraft.MvvmEssentials.Services.INavigationParameters parameters)"); + sb.AppendLine(" {"); + var i = 0; + foreach (var p in t.Parameters.Array) + { + var v = "__v" + i++; + sb.Append(" if (parameters.TryGetValue<").Append(p.PropertyType).Append(">(\"") + .Append(p.PropertyName).Append("\", out var ").Append(v).Append(")) this.") + .Append(p.PropertyName).Append(" = ").Append(v).AppendLine("!;"); + } + sb.AppendLine(" }"); + + sb.AppendLine("}"); + + var hint = (t.Namespace.Length > 0 ? t.Namespace + "." : string.Empty) + t.ClassName + ".NavigationParameters.g.cs"; + spc.AddSource(hint, sb.ToString()); + } + + private static bool IsDerivedFrom(INamedTypeSymbol symbol, string baseTypeName) + { + for (var t = symbol; t is not null; t = t.BaseType) + if (t.ToDisplayString() == baseTypeName) + return true; + return false; + } +} + +// arrays don't have structural equality, which silently breaks incremental caching. +// this wrapper restores it so the pipeline actually memoizes per-VM. +internal readonly struct EquatableArray(T[] array) : IEquatable> where T : IEquatable +{ + public readonly T[] Array = array; + + public bool Equals(EquatableArray other) + { + if (Array is null) return false; + if (Array.Length != other.Array.Length) return false; + return Array.Where((t, i) => !t.Equals(other.Array[i])).Any() == false; + } + + public override bool Equals(object? obj) => obj is EquatableArray o && Equals(o); + + public override int GetHashCode() + { + if (Array is null) return 0; + unchecked + { + return Array.Aggregate(17, (current, item) => current * 31 + item.GetHashCode()); + } + } +} \ No newline at end of file diff --git a/Source/SourceGenerator/Nkraft.MvvmEssentials.SourceGenerator.csproj b/src/SourceGenerator/Nkraft.MvvmEssentials.SourceGenerator.csproj similarity index 100% rename from Source/SourceGenerator/Nkraft.MvvmEssentials.SourceGenerator.csproj rename to src/SourceGenerator/Nkraft.MvvmEssentials.SourceGenerator.csproj diff --git a/Source/ViewModels/BaseViewModel.cs b/src/ViewModels/BaseViewModel.cs similarity index 100% rename from Source/ViewModels/BaseViewModel.cs rename to src/ViewModels/BaseViewModel.cs diff --git a/Source/ViewModels/FlyoutHostViewModel.cs b/src/ViewModels/FlyoutHostViewModel.cs similarity index 100% rename from Source/ViewModels/FlyoutHostViewModel.cs rename to src/ViewModels/FlyoutHostViewModel.cs diff --git a/Source/ViewModels/FlyoutMenuViewModel.cs b/src/ViewModels/FlyoutMenuViewModel.cs similarity index 100% rename from Source/ViewModels/FlyoutMenuViewModel.cs rename to src/ViewModels/FlyoutMenuViewModel.cs diff --git a/Source/ViewModels/NavigableEntryViewModel.cs b/src/ViewModels/NavigableEntryViewModel.cs similarity index 80% rename from Source/ViewModels/NavigableEntryViewModel.cs rename to src/ViewModels/NavigableEntryViewModel.cs index 3a9332c..cb69202 100644 --- a/Source/ViewModels/NavigableEntryViewModel.cs +++ b/src/ViewModels/NavigableEntryViewModel.cs @@ -1,4 +1,6 @@ using System.Reflection; +using Nkraft.CrossUtility.Helpers; +using Nkraft.MvvmEssentials.Attributes; using Nkraft.MvvmEssentials.Services; using Nkraft.MvvmEssentials.Services.Navigation; using Nkraft.MvvmEssentials.Services.Pages; @@ -12,9 +14,10 @@ public abstract class NavigableEntryViewModel : BaseViewModel, { internal void SetNavigationParameter(string key, object? value) { - var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; var property = GetType().GetProperty(key, bindingFlags); - if (property is null || property.CanWrite == false) + var hasParameterAttribute = property?.GetCustomAttributes().Any(); + if (hasParameterAttribute == false || property is null || property.CanWrite == false) return; if (value is null || AreTypesEqual(property.PropertyType, value.GetType())) diff --git a/Source/ViewModels/PageViewModel.cs b/src/ViewModels/PageViewModel.cs similarity index 100% rename from Source/ViewModels/PageViewModel.cs rename to src/ViewModels/PageViewModel.cs diff --git a/Source/ViewModels/PopupViewModel.cs b/src/ViewModels/PopupViewModel.cs similarity index 100% rename from Source/ViewModels/PopupViewModel.cs rename to src/ViewModels/PopupViewModel.cs diff --git a/Source/ViewModels/TabHostViewModel.cs b/src/ViewModels/TabHostViewModel.cs similarity index 92% rename from Source/ViewModels/TabHostViewModel.cs rename to src/ViewModels/TabHostViewModel.cs index 9acdd08..d369b09 100644 --- a/Source/ViewModels/TabHostViewModel.cs +++ b/src/ViewModels/TabHostViewModel.cs @@ -1,4 +1,5 @@ using Nkraft.CrossUtility.Patterns; +using Nkraft.MvvmEssentials.Attributes; using Nkraft.MvvmEssentials.Helpers; using Nkraft.MvvmEssentials.Services; using Nkraft.MvvmEssentials.Services.Navigation; @@ -6,7 +7,7 @@ namespace Nkraft.MvvmEssentials.ViewModels; -public abstract class TabHostViewModel : PageViewModel, ITabHost +public abstract partial class TabHostViewModel : PageViewModel, ITabHost { protected override void OnInitialized() { @@ -40,6 +41,7 @@ protected async Task SwitchTabAsync( protected ITabComponent CurrentTab => Tabs.ElementAt(SelectedTabIndex); + [Parameter] protected int SelectedTabIndex { get; set; } IReadOnlyCollection ITabHost.Tabs => Tabs; diff --git a/Source/ViewModels/TabViewModel.cs b/src/ViewModels/TabViewModel.cs similarity index 100% rename from Source/ViewModels/TabViewModel.cs rename to src/ViewModels/TabViewModel.cs diff --git a/Source/ViewModels/WizardHostViewModel.cs b/src/ViewModels/WizardHostViewModel.cs similarity index 100% rename from Source/ViewModels/WizardHostViewModel.cs rename to src/ViewModels/WizardHostViewModel.cs diff --git a/Source/ViewModels/WizardStepViewModel.cs b/src/ViewModels/WizardStepViewModel.cs similarity index 100% rename from Source/ViewModels/WizardStepViewModel.cs rename to src/ViewModels/WizardStepViewModel.cs diff --git a/Tests/Extensions/NavigationServiceExtensionTest.cs b/tests/Extensions/NavigationServiceExtensionTest.cs similarity index 100% rename from Tests/Extensions/NavigationServiceExtensionTest.cs rename to tests/Extensions/NavigationServiceExtensionTest.cs diff --git a/Tests/Fakes/Fakes.cs b/tests/Fakes/Fakes.cs similarity index 100% rename from Tests/Fakes/Fakes.cs rename to tests/Fakes/Fakes.cs diff --git a/Tests/Helpers/PageHelperTests.cs b/tests/Helpers/PageHelperTests.cs similarity index 100% rename from Tests/Helpers/PageHelperTests.cs rename to tests/Helpers/PageHelperTests.cs diff --git a/Tests/Nkraft.MvvmEssentials.UnitTest.csproj b/tests/Nkraft.MvvmEssentials.UnitTest.csproj similarity index 78% rename from Tests/Nkraft.MvvmEssentials.UnitTest.csproj rename to tests/Nkraft.MvvmEssentials.UnitTest.csproj index c699ae0..201a336 100644 --- a/Tests/Nkraft.MvvmEssentials.UnitTest.csproj +++ b/tests/Nkraft.MvvmEssentials.UnitTest.csproj @@ -9,9 +9,11 @@ + + diff --git a/Tests/Services/AppStartupWindowHookTests.cs b/tests/Services/AppStartupWindowHookTests.cs similarity index 100% rename from Tests/Services/AppStartupWindowHookTests.cs rename to tests/Services/AppStartupWindowHookTests.cs diff --git a/Tests/Services/Navigation/NavigationParametersTests.cs b/tests/Services/Navigation/NavigationParametersTests.cs similarity index 100% rename from Tests/Services/Navigation/NavigationParametersTests.cs rename to tests/Services/Navigation/NavigationParametersTests.cs diff --git a/Tests/Services/Navigation/PageLinkTests.cs b/tests/Services/Navigation/PageLinkTests.cs similarity index 100% rename from Tests/Services/Navigation/PageLinkTests.cs rename to tests/Services/Navigation/PageLinkTests.cs diff --git a/Tests/Services/Navigation/PageRegistryTests.cs b/tests/Services/Navigation/PageRegistryTests.cs similarity index 100% rename from Tests/Services/Navigation/PageRegistryTests.cs rename to tests/Services/Navigation/PageRegistryTests.cs diff --git a/Tests/Services/NavigationServiceTests.cs b/tests/Services/NavigationServiceTests.cs similarity index 100% rename from Tests/Services/NavigationServiceTests.cs rename to tests/Services/NavigationServiceTests.cs diff --git a/Tests/ViewModels/BaseViewModelTests.cs b/tests/ViewModels/BaseViewModelTests.cs similarity index 100% rename from Tests/ViewModels/BaseViewModelTests.cs rename to tests/ViewModels/BaseViewModelTests.cs diff --git a/Tests/ViewModels/FlyoutMenuViewModelTests.cs b/tests/ViewModels/FlyoutMenuViewModelTests.cs similarity index 100% rename from Tests/ViewModels/FlyoutMenuViewModelTests.cs rename to tests/ViewModels/FlyoutMenuViewModelTests.cs diff --git a/Tests/ViewModels/NavigableEntryViewModelTests.cs b/tests/ViewModels/NavigableEntryViewModelTests.cs similarity index 100% rename from Tests/ViewModels/NavigableEntryViewModelTests.cs rename to tests/ViewModels/NavigableEntryViewModelTests.cs diff --git a/tests/ViewModels/NavigationParametersGeneratorTests.cs b/tests/ViewModels/NavigationParametersGeneratorTests.cs new file mode 100644 index 0000000..0f911e8 --- /dev/null +++ b/tests/ViewModels/NavigationParametersGeneratorTests.cs @@ -0,0 +1,58 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Nkraft.MvvmEssentials.SourceGenerator; +using NUnit.Framework; + +namespace Nkraft.MvvmEssentials.UnitTest.ViewModels; + +[TestFixture] +public class NavigationParametersGeneratorTests +{ + [Test] + public void Generator_ShouldGenerateRecord_WhenClassHasParameterAttribute() + { + var source = """ + using Nkraft.MvvmEssentials.ViewModels; + using Nkraft.MvvmEssentials.Attributes; + + public class MyViewModel : NavigableEntryViewModel + { + [Parameter] + public string Name { get; set; } + } + """; + + var dotNetAssemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) + .Select(a => MetadataReference.CreateFromFile(a.Location)); + + var mvvmEssentialsAssembly = typeof(Nkraft.MvvmEssentials.ViewModels.NavigableEntryViewModel).Assembly.Location; + + // 1. Create an EMPTY compilation baseline first + var baselineCompilation = CSharpCompilation.Create( + assemblyName: "TestAssembly", + syntaxTrees: [CSharpSyntaxTree.ParseText(source)], + references: [ + ..dotNetAssemblies, + MetadataReference.CreateFromFile(mvvmEssentialsAssembly) // Ensure this is explicitly linked! + ], + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ); + + var generator = new NavigationParametersGenerator(); + GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()]); + + // 2. Run the driver on the empty baseline to initialize its internal cache + driver = driver.RunGenerators(baselineCompilation); + + // 3. NOW add your source file to the compilation (creating a delta change) + var updatedCompilation = baselineCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)); + + // 4. Run the driver again. Roslyn sees the new tree and forces the predicate to run! + driver = driver.RunGeneratorsAndUpdateCompilation(updatedCompilation, out var outputCompilation, out var diagnostics); + + // Verify results + var runResult = driver.GetRunResult(); + Assert.That(runResult.GeneratedTrees.Length, Is.GreaterThan(0), "No source files were generated!"); + } +} \ No newline at end of file diff --git a/Tests/ViewModels/PageViewModelTests.cs b/tests/ViewModels/PageViewModelTests.cs similarity index 100% rename from Tests/ViewModels/PageViewModelTests.cs rename to tests/ViewModels/PageViewModelTests.cs diff --git a/Tests/ViewModels/PopupViewModelTests.cs b/tests/ViewModels/PopupViewModelTests.cs similarity index 100% rename from Tests/ViewModels/PopupViewModelTests.cs rename to tests/ViewModels/PopupViewModelTests.cs diff --git a/Tests/ViewModels/TabHostViewModelTests.cs b/tests/ViewModels/TabHostViewModelTests.cs similarity index 100% rename from Tests/ViewModels/TabHostViewModelTests.cs rename to tests/ViewModels/TabHostViewModelTests.cs diff --git a/Tests/ViewModels/TabViewModelTests.cs b/tests/ViewModels/TabViewModelTests.cs similarity index 100% rename from Tests/ViewModels/TabViewModelTests.cs rename to tests/ViewModels/TabViewModelTests.cs