diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ceb93a..3857009 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,48 @@ jobs: path: ./test-results/results.trx if-no-files-found: warn + test-e2e: + name: E2E (Avalonia headless) / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props') }} + restore-keys: ${{ runner.os }}-nuget- + + - name: Restore + run: dotnet restore tests/AsyncNavigation.E2E.Tests + + - name: Test + run: > + dotnet test tests/AsyncNavigation.E2E.Tests + --configuration Release + --no-restore + --logger "console;verbosity=normal" + --logger "trx;LogFileName=results.trx" + --results-directory ./test-results + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() # upload even when tests fail so we can inspect + with: + name: e2e-test-results-${{ matrix.os }} + path: ./test-results/results.trx + if-no-files-found: warn + build-libs: name: Build Libraries runs-on: ubuntu-latest @@ -199,6 +241,8 @@ jobs: - name: Setup Android SDK if: matrix.platform == 'Android' uses: android-actions/setup-android@v3 + with: + packages: platform-tools - name: Install Android workload if: matrix.platform == 'Android' diff --git a/readme.md b/readme.md index 63d6e23..3adb397 100644 --- a/readme.md +++ b/readme.md @@ -94,14 +94,32 @@ var result = await _dialogService.ShowViewDialogAsync("Confirm"); ### 4. React to navigation in view models +View models implement `INavigationAware` directly. `NavigationAwareBase` remains available for compatibility but is obsolete; new code should implement `INavigationAware`. You can define an application base class to share default implementations. + ```csharp -// Derive from NavigationAwareBase and override only what you need. -public class HomeViewModel : NavigationAwareBase +using AsyncNavigation; +using AsyncNavigation.Abstractions; +using AsyncNavigation.Core; +using System.Threading; +using System.Threading.Tasks; + +public class HomeViewModel : INavigationAware { - public override async Task OnNavigatedToAsync(NavigationContext context) + public event AsyncEventHandler? AsyncRequestUnloadEvent; + + public Task InitializeAsync(NavigationContext context) => Task.CompletedTask; + + public Task OnNavigatedToAsync(NavigationContext context) { - await LoadDataAsync(context.CancellationToken); + // Load page data here; use context.CancellationToken for async operations. + return Task.CompletedTask; } + + public Task OnNavigatedFromAsync(NavigationContext context) => Task.CompletedTask; + public Task OnUnloadAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + // Allow reuse when caching is enabled; return false to request a new instance. + public Task IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true); } ``` @@ -110,12 +128,21 @@ public class HomeViewModel : NavigationAwareBase ## Navigation Guard ```csharp -public class EditViewModel : NavigationAwareBase, INavigationGuard +public class EditViewModel : INavigationAware, INavigationGuard { - public async Task CanNavigateAsync(NavigationContext context, CancellationToken ct) + public bool HasUnsavedChanges { get; set; } + public event AsyncEventHandler? AsyncRequestUnloadEvent; + + public Task InitializeAsync(NavigationContext context) => Task.CompletedTask; + public Task OnNavigatedToAsync(NavigationContext context) => Task.CompletedTask; + public Task OnNavigatedFromAsync(NavigationContext context) => Task.CompletedTask; + public Task OnUnloadAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true); + + public Task CanNavigateAsync(NavigationContext context, CancellationToken ct) { - // Return false to cancel — show a confirmation dialog here if needed. - return !HasUnsavedChanges; + // Return false to cancel; show a confirmation dialog asynchronously here if needed. + return Task.FromResult(!HasUnsavedChanges); } } ``` diff --git a/readme_zh-cn.md b/readme_zh-cn.md index 90be970..11ade05 100644 --- a/readme_zh-cn.md +++ b/readme_zh-cn.md @@ -94,14 +94,32 @@ var result = await _dialogService.ShowViewDialogAsync("Confirm"); ### 4. 在视图模型中响应导航 +视图模型直接实现 `INavigationAware`。`NavigationAwareBase` 为兼容旧代码而保留,但已标记弃用,建议新代码实现 `INavigationAware`。如需复用默认实现,可以在应用中定义自己的基类。 + ```csharp -// 继承 NavigationAwareBase,按需重写方法即可。 -public class HomeViewModel : NavigationAwareBase +using AsyncNavigation; +using AsyncNavigation.Abstractions; +using AsyncNavigation.Core; +using System.Threading; +using System.Threading.Tasks; + +public class HomeViewModel : INavigationAware { - public override async Task OnNavigatedToAsync(NavigationContext context) + public event AsyncEventHandler? AsyncRequestUnloadEvent; + + public Task InitializeAsync(NavigationContext context) => Task.CompletedTask; + + public Task OnNavigatedToAsync(NavigationContext context) { - await LoadDataAsync(context.CancellationToken); + // 在此加载页面数据;异步操作可使用 context.CancellationToken。 + return Task.CompletedTask; } + + public Task OnNavigatedFromAsync(NavigationContext context) => Task.CompletedTask; + public Task OnUnloadAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + // 启用缓存时,允许复用此实例;返回 false 可请求新实例。 + public Task IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true); } ``` @@ -110,12 +128,21 @@ public class HomeViewModel : NavigationAwareBase ## 导航守卫 ```csharp -public class EditViewModel : NavigationAwareBase, INavigationGuard +public class EditViewModel : INavigationAware, INavigationGuard { - public async Task CanNavigateAsync(NavigationContext context, CancellationToken ct) + public bool HasUnsavedChanges { get; set; } + public event AsyncEventHandler? AsyncRequestUnloadEvent; + + public Task InitializeAsync(NavigationContext context) => Task.CompletedTask; + public Task OnNavigatedToAsync(NavigationContext context) => Task.CompletedTask; + public Task OnNavigatedFromAsync(NavigationContext context) => Task.CompletedTask; + public Task OnUnloadAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true); + + public Task CanNavigateAsync(NavigationContext context, CancellationToken ct) { - // 返回 false 可取消导航,通常在此弹出确认对话框 - return !HasUnsavedChanges; + // 返回 false 可取消导航;如需确认,可在此异步显示对话框。 + return Task.FromResult(!HasUnsavedChanges); } } ``` diff --git a/samples/Sample.Avalonia.iOS/Info.plist b/samples/Sample.Avalonia.iOS/Info.plist index 3fff6e2..91a3c19 100644 --- a/samples/Sample.Avalonia.iOS/Info.plist +++ b/samples/Sample.Avalonia.iOS/Info.plist @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 13.0 + 15.0 UIDeviceFamily 1 diff --git a/samples/Sample.Avalonia.iOS/Sample.Avalonia.iOS.csproj b/samples/Sample.Avalonia.iOS/Sample.Avalonia.iOS.csproj index cefb484..08ce307 100644 --- a/samples/Sample.Avalonia.iOS/Sample.Avalonia.iOS.csproj +++ b/samples/Sample.Avalonia.iOS/Sample.Avalonia.iOS.csproj @@ -2,7 +2,7 @@ Exe net10.0-ios - 13.0 + 15.0 enable false diff --git a/samples/Sample.Avalonia/App.axaml.cs b/samples/Sample.Avalonia/App.axaml.cs index da64262..5bb8a3b 100644 --- a/samples/Sample.Avalonia/App.axaml.cs +++ b/samples/Sample.Avalonia/App.axaml.cs @@ -30,6 +30,7 @@ public override void OnFrameworkInitializationCompleted() var services = new ServiceCollection(); services.AddNavigationSupport(navigationOptions) + .AddFloatingSupport() .AddSingletonWithAllMembers() .RegisterView(nameof(LightView)) .RegisterView(nameof(ItemsRegionView)) @@ -74,7 +75,7 @@ public override void OnFrameworkInitializationCompleted() if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow + desktop.MainWindow = new MainWindow(sp.GetRequiredService()) { DataContext = sp.GetRequiredService() }; @@ -82,11 +83,11 @@ public override void OnFrameworkInitializationCompleted() } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { - singleViewPlatform.MainView = new MainView + singleViewPlatform.MainView = new MainView(sp.GetRequiredService()) { DataContext = sp.GetRequiredService() }; } #endregion } -} \ No newline at end of file +} diff --git a/samples/Sample.Avalonia/Sample.Avalonia.csproj b/samples/Sample.Avalonia/Sample.Avalonia.csproj index 91ca3b0..3c65a09 100644 --- a/samples/Sample.Avalonia/Sample.Avalonia.csproj +++ b/samples/Sample.Avalonia/Sample.Avalonia.csproj @@ -30,6 +30,7 @@ + diff --git a/samples/Sample.Avalonia/Views/ChildContentRegionView.axaml b/samples/Sample.Avalonia/Views/ChildContentRegionView.axaml index 1208232..0267aaa 100644 --- a/samples/Sample.Avalonia/Views/ChildContentRegionView.axaml +++ b/samples/Sample.Avalonia/Views/ChildContentRegionView.axaml @@ -39,6 +39,10 @@ Classes="Navigation" Content="ToHeavyView" Tag="HeavyView" /> +