Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
43 changes: 35 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<AsyncEventArgs>? 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<bool> IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true);
}
```

Expand All @@ -110,12 +128,21 @@ public class HomeViewModel : NavigationAwareBase
## Navigation Guard

```csharp
public class EditViewModel : NavigationAwareBase, INavigationGuard
public class EditViewModel : INavigationAware, INavigationGuard
{
public async Task<bool> CanNavigateAsync(NavigationContext context, CancellationToken ct)
public bool HasUnsavedChanges { get; set; }
public event AsyncEventHandler<AsyncEventArgs>? 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<bool> IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true);

public Task<bool> 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);
}
}
```
Expand Down
43 changes: 35 additions & 8 deletions readme_zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<AsyncEventArgs>? 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<bool> IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true);
}
```

Expand All @@ -110,12 +128,21 @@ public class HomeViewModel : NavigationAwareBase
## 导航守卫

```csharp
public class EditViewModel : NavigationAwareBase, INavigationGuard
public class EditViewModel : INavigationAware, INavigationGuard
{
public async Task<bool> CanNavigateAsync(NavigationContext context, CancellationToken ct)
public bool HasUnsavedChanges { get; set; }
public event AsyncEventHandler<AsyncEventArgs>? 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<bool> IsNavigationTargetAsync(NavigationContext context) => Task.FromResult(true);

public Task<bool> CanNavigateAsync(NavigationContext context, CancellationToken ct)
{
// 返回 false 可取消导航,通常在此弹出确认对话框
return !HasUnsavedChanges;
// 返回 false 可取消导航;如需确认,可在此异步显示对话框。
return Task.FromResult(!HasUnsavedChanges);
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion samples/Sample.Avalonia.iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>13.0</string>
<string>15.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
Expand Down
2 changes: 1 addition & 1 deletion samples/Sample.Avalonia.iOS/Sample.Avalonia.iOS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-ios</TargetFramework>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ValidateXcodeVersion>false</ValidateXcodeVersion>
</PropertyGroup>
Expand Down
7 changes: 4 additions & 3 deletions samples/Sample.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public override void OnFrameworkInitializationCompleted()

var services = new ServiceCollection();
services.AddNavigationSupport(navigationOptions)
.AddFloatingSupport()
.AddSingletonWithAllMembers<MainWindowViewModel>()
.RegisterView<LightView, LightViewModel>(nameof(LightView))
.RegisterView<ItemsRegionView, ItemsRegionViewModel>(nameof(ItemsRegionView))
Expand Down Expand Up @@ -74,19 +75,19 @@ public override void OnFrameworkInitializationCompleted()

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
desktop.MainWindow = new MainWindow(sp.GetRequiredService<AsyncNavigation.Floating.IViewPlacementService>())
{
DataContext = sp.GetRequiredService<MainWindowViewModel>()
};
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
singleViewPlatform.MainView = new MainView(sp.GetRequiredService<AsyncNavigation.Floating.IViewPlacementService>())
{
DataContext = sp.GetRequiredService<MainWindowViewModel>()
};
}
#endregion
}
}
}
1 change: 1 addition & 0 deletions samples/Sample.Avalonia/Sample.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\AsyncNavigation.Avalonia\AsyncNavigation.Avalonia.csproj" />
<ProjectReference Include="..\..\src\AsyncNavigation.Avalonia.Floating\AsyncNavigation.Avalonia.Floating.csproj" />
<ProjectReference Include="..\Sample.Common\Sample.Common.csproj" />
</ItemGroup>

Expand Down
4 changes: 4 additions & 0 deletions samples/Sample.Avalonia/Views/ChildContentRegionView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
Classes="Navigation"
Content="ToHeavyView"
Tag="HeavyView" />
<Button
Padding="8,4"
Click="FloatChildRegion_Click"
Content="Floating: current ChildContentRegion view" />
</StackPanel>
<ContentControl an:RegionManager.RegionName="ChildContentRegion" />
</DockPanel>
Expand Down
37 changes: 34 additions & 3 deletions samples/Sample.Avalonia/Views/ChildContentRegionView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
using AsyncNavigation.Abstractions;
using AsyncNavigation.Abstractions;
using AsyncNavigation.Floating;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
using System.Diagnostics;

namespace Sample.Avalonia.Views;

public partial class ChildContentRegionView : UserControl, IView
{
public ChildContentRegionView()
private readonly IViewPlacementService? _placementService;

public ChildContentRegionView() : this(null)
{
}

public ChildContentRegionView(IViewPlacementService? placementService)
{
_placementService = placementService;
InitializeComponent();
}
}

private async void FloatChildRegion_Click(object? sender, RoutedEventArgs e)
{
if (_placementService is null)
return;

try
{
await _placementService.FloatAsync("ChildContentRegion", options: new FloatingWindowOptions
{
Title = "AsyncNavigation floating ChildContentRegion",
Width = 720,
Height = 480
});
}
catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)
{
Debug.WriteLine($"Cannot float ChildContentRegion: {ex.Message}");
}
}
}
18 changes: 14 additions & 4 deletions samples/Sample.Avalonia/Views/HeavyView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,25 @@
<Button Classes="CloseDialog" />
<ItemsControl ItemsSource="{Binding HeavyItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate x:DataType="vm:HeavyItemViewModel">
<Border
Margin="2"
BorderBrush="Black"
BorderThickness="1">
<DockPanel LastChildFill="True">
<ComboBox Width="50" />
<Expander Width="50" />
<DatePicker Width="50" />
<ComboBox
Width="100"
ItemsSource="{Binding Options}"
SelectedItem="{Binding SelectedOption}" />
<Expander
Width="100"
Header="Details"
IsExpanded="{Binding IsExpanded}">
<TextBlock Text="{Binding Value}" />
</Expander>
<DatePicker
Width="140"
SelectedDate="{Binding AvaloniaSelectedDate}" />
</DockPanel>
</Border>
</DataTemplate>
Expand Down
4 changes: 4 additions & 0 deletions samples/Sample.Avalonia/Views/ItemsRegionView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
Classes="NavigationAndForget"
Content="ToLightView:Forget"
Tag="LightView" />
<Button
Padding="8,4"
Click="FloatSelectedItem_Click"
Content="Floating: selected ItemsRegion view" />
</StackPanel>
<ScrollViewer>
<ItemsControl an:RegionManager.RegionName="ItemsRegion">
Expand Down
35 changes: 30 additions & 5 deletions samples/Sample.Avalonia/Views/ItemsRegionView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
using AsyncNavigation.Abstractions;
using Avalonia;
using AsyncNavigation.Floating;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
using System.Diagnostics;

namespace Sample.Avalonia.Views;

public partial class ItemsRegionView : UserControl, IView
{
public ItemsRegionView()
private readonly IViewPlacementService? _placementService;

public ItemsRegionView() : this(null)
{
}

public ItemsRegionView(IViewPlacementService? placementService)
{
_placementService = placementService;
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
private async void FloatSelectedItem_Click(object? sender, RoutedEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_placementService is null)
return;

try
{
await _placementService.FloatAsync("ItemsRegion", options: new FloatingWindowOptions
{
Title = "AsyncNavigation floating ItemsRegion item",
Width = 720,
Height = 480
});
}
catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)
{
Debug.WriteLine($"Cannot float ItemsRegion: {ex.Message}");
}
}
}
}
Loading
Loading