Skip to content

Commit 26c7c4e

Browse files
committed
feat(NavigationService): exception handling && decouple by using mvvm toolkit messenger
1 parent 5f77b83 commit 26c7c4e

File tree

7 files changed

+186
-103
lines changed

7 files changed

+186
-103
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using PCL.Neo.Services;
2+
using PCL.Neo.ViewModels;
3+
using System;
4+
5+
namespace PCL.Neo.Messages;
6+
7+
public class NavigationMessage(
8+
ViewModelBase? oldMainViewModel,
9+
ViewModelBase? newMainViewModel,
10+
ViewModelBase? oldSubViewModel,
11+
ViewModelBase? newSubViewModel,
12+
NavigationType navigationType)
13+
{
14+
public static class Channels
15+
{
16+
public static readonly Guid Navigating = Guid.NewGuid();
17+
public static readonly Guid Navigated = Guid.NewGuid();
18+
}
19+
20+
public bool IsMainViewModelChanged => oldMainViewModel != newMainViewModel;
21+
public ViewModelBase? OldMainViewModel => oldMainViewModel;
22+
public ViewModelBase? NewMainViewModel => newMainViewModel;
23+
public bool IsSubViewModelChanged => oldSubViewModel != newSubViewModel;
24+
public ViewModelBase? OldSubViewModel => oldSubViewModel;
25+
public ViewModelBase? NewSubViewModel => newSubViewModel;
26+
public NavigationType NavigationType => navigationType;
27+
}

PCL.Neo/Services/NavigationService.cs

Lines changed: 91 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using CommunityToolkit.Mvvm.Messaging;
12
using Microsoft.Extensions.DependencyInjection;
3+
using PCL.Neo.Messages;
24
using PCL.Neo.ViewModels;
35
using System;
46
using System.Collections.Generic;
@@ -9,8 +11,8 @@ namespace PCL.Neo.Services;
911

1012
public interface INavigationService
1113
{
12-
public event Action<NavigationEventArgs>? Navigating;
13-
public event Action<NavigationEventArgs>? Navigated;
14+
// public event Action<NavigationMessage>? Navigating;
15+
// public event Action<NavigationMessage>? Navigated;
1416

1517
public ViewModelBase? CurrentMainViewModel { get; }
1618

@@ -30,28 +32,12 @@ public enum NavigationType
3032
Backward
3133
}
3234

33-
public class NavigationEventArgs(
34-
ViewModelBase? oldMainViewModel,
35-
ViewModelBase? newMainViewModel,
36-
ViewModelBase? oldSubViewModel,
37-
ViewModelBase? newSubViewModel,
38-
NavigationType navigationType)
39-
{
40-
public bool IsMainViewModelChanged => oldMainViewModel != newMainViewModel;
41-
public ViewModelBase? OldMainViewModel => oldMainViewModel;
42-
public ViewModelBase? NewMainViewModel => newMainViewModel;
43-
public bool IsSubViewModelChanged => oldSubViewModel != newSubViewModel;
44-
public ViewModelBase? OldSubViewModel => oldSubViewModel;
45-
public ViewModelBase? NewSubViewModel => newSubViewModel;
46-
public NavigationType NavigationType => navigationType;
47-
}
48-
4935
public class NavigationService(IServiceProvider serviceProvider) : INavigationService
5036
{
5137
public IServiceProvider ServiceProvider { get; } = serviceProvider;
5238

53-
public event Action<NavigationEventArgs>? Navigating;
54-
public event Action<NavigationEventArgs>? Navigated;
39+
// public event Action<NavigationMessage>? Navigating;
40+
// public event Action<NavigationMessage>? Navigated;
5541

5642
// 导航历史记录
5743
private readonly LinkedList<(Type?, Type?)> _navigationHistory = [];
@@ -73,40 +59,50 @@ public class NavigationService(IServiceProvider serviceProvider) : INavigationSe
7359
/// <exception cref="InvalidOperationException"></exception>
7460
public async Task<(ViewModelBase? mainVm, ViewModelBase? subVm)> GoToAsync<T>() where T : ViewModelBase
7561
{
76-
// T 可为 `MainViewModel` 或 `SubViewModel`
77-
// 根据 T 上附加的 attribute 判断
78-
var mainAttr = typeof(T).GetCustomAttribute<MainViewModelAttribute>();
79-
var subAttr = typeof(T).GetCustomAttribute<SubViewModelAttribute>();
80-
if (mainAttr is null && subAttr is null)
81-
throw new InvalidOperationException(
82-
$"ViewModel {typeof(T).Name} does not have a {nameof(MainViewModelAttribute)} or a {nameof(SubViewModelAttribute)}");
83-
if (mainAttr is not null && subAttr is not null)
84-
throw new InvalidOperationException(
85-
$"ViewModel {typeof(T).Name} has both {nameof(MainViewModelAttribute)} and {nameof(SubViewModelAttribute)}");
86-
87-
Type? mainVmType;
88-
Type? subVmType;
89-
if (mainAttr is not null)
62+
try
9063
{
91-
mainVmType = typeof(T);
92-
subVmType = mainAttr.DefaultSubViewModelType;
64+
// T 可为 `MainViewModel` 或 `SubViewModel`
65+
// 根据 T 上附加的 attribute 判断
66+
var mainAttr = typeof(T).GetCustomAttribute<MainViewModelAttribute>();
67+
var subAttr = typeof(T).GetCustomAttribute<SubViewModelAttribute>();
68+
if (mainAttr is null && subAttr is null)
69+
throw new InvalidOperationException(
70+
$"ViewModel {typeof(T).Name} does not have a {nameof(MainViewModelAttribute)} or a {nameof(SubViewModelAttribute)}");
71+
if (mainAttr is not null && subAttr is not null)
72+
throw new InvalidOperationException(
73+
$"ViewModel {typeof(T).Name} has both {nameof(MainViewModelAttribute)} and {nameof(SubViewModelAttribute)}");
74+
75+
Type? mainVmType;
76+
Type? subVmType;
77+
if (mainAttr is not null)
78+
{
79+
mainVmType = typeof(T);
80+
subVmType = mainAttr.DefaultSubViewModelType;
81+
}
82+
else // if (subAttr is not null)
83+
{
84+
mainVmType = subAttr!.MainViewModelType;
85+
subVmType = typeof(T);
86+
}
87+
88+
var mainVm = CurrentMainViewModel?.GetType() == mainVmType
89+
? CurrentMainViewModel
90+
: (ViewModelBase)ServiceProvider.GetRequiredService(mainVmType);
91+
var subVm = CurrentSubViewModel?.GetType() == subVmType
92+
? CurrentSubViewModel
93+
: (ViewModelBase)ServiceProvider.GetRequiredService(subVmType);
94+
95+
await NavigateToAsync(mainVm, subVm);
96+
97+
return (mainVm, subVm);
9398
}
94-
else // if (subAttr is not null)
99+
catch (Exception ex)
95100
{
96-
mainVmType = subAttr!.MainViewModelType;
97-
subVmType = typeof(T);
101+
// TODO: logger
102+
Console.WriteLine($"[{nameof(NavigationService)}] navigation failed: {ex}");
98103
}
99104

100-
var mainVm = CurrentMainViewModel?.GetType() == mainVmType
101-
? CurrentMainViewModel
102-
: (ViewModelBase)ServiceProvider.GetRequiredService(mainVmType);
103-
var subVm = CurrentSubViewModel?.GetType() == subVmType
104-
? CurrentSubViewModel
105-
: (ViewModelBase)ServiceProvider.GetRequiredService(subVmType);
106-
107-
await NavigateToAsync(mainVm, subVm);
108-
109-
return (mainVm, subVm);
105+
return (null, null);
110106
}
111107

112108
/// <summary>
@@ -117,16 +113,26 @@ public class NavigationService(IServiceProvider serviceProvider) : INavigationSe
117113
/// <returns>(MainViewModel, SubViewModel)</returns>
118114
public async Task<(TM?, TS?)> GoToAsync<TM, TS>() where TM : ViewModelBase where TS : ViewModelBase
119115
{
120-
var mainVm = CurrentMainViewModel?.GetType() == typeof(TM)
121-
? CurrentMainViewModel as TM
122-
: ServiceProvider.GetRequiredService<TM>();
123-
var subVm = CurrentSubViewModel?.GetType() == typeof(TS)
124-
? CurrentSubViewModel as TS
125-
: ServiceProvider.GetRequiredService<TS>();
116+
try
117+
{
118+
var mainVm = CurrentMainViewModel?.GetType() == typeof(TM)
119+
? CurrentMainViewModel as TM
120+
: ServiceProvider.GetRequiredService<TM>();
121+
var subVm = CurrentSubViewModel?.GetType() == typeof(TS)
122+
? CurrentSubViewModel as TS
123+
: ServiceProvider.GetRequiredService<TS>();
126124

127-
await NavigateToAsync(mainVm, subVm);
125+
await NavigateToAsync(mainVm, subVm);
128126

129-
return (mainVm, subVm);
127+
return (mainVm, subVm);
128+
}
129+
catch (Exception ex)
130+
{
131+
// TODO: logger
132+
Console.WriteLine($"[{nameof(NavigationService)}] navigation failed: {ex}");
133+
}
134+
135+
return (null, null);
130136
}
131137

132138
/// <summary>
@@ -139,25 +145,33 @@ protected async Task NavigateToAsync(ViewModelBase? main, ViewModelBase? sub,
139145
NavigationType navigationType = NavigationType.Forward)
140146
{
141147
var oldMainVm = CurrentMainViewModel;
142-
var oldSubVm = CurrentSubViewModel;
148+
var oldSubVm = CurrentSubViewModel;
143149

144-
Navigating?.Invoke(new NavigationEventArgs(
150+
// Navigating?.Invoke(new NavigationMessage(
151+
// oldMainVm, main,
152+
// oldSubVm, sub,
153+
// navigationType));
154+
WeakReferenceMessenger.Default.Send(new NavigationMessage(
145155
oldMainVm, main,
146156
oldSubVm, sub,
147-
navigationType));
157+
navigationType), NavigationMessage.Channels.Navigating);
148158

149159
if (navigationType == NavigationType.Forward)
150160
PushHistory(
151161
oldMainVm?.GetType() ?? null,
152162
oldSubVm?.GetType() ?? null);
153163

154164
CurrentMainViewModel = main;
155-
CurrentSubViewModel = sub;
165+
CurrentSubViewModel = sub;
156166

157-
Navigated?.Invoke(new NavigationEventArgs(
167+
// Navigated?.Invoke(new NavigationMessage(
168+
// oldMainVm, main,
169+
// oldSubVm, sub,
170+
// navigationType));
171+
WeakReferenceMessenger.Default.Send(new NavigationMessage(
158172
oldMainVm, main,
159173
oldSubVm, sub,
160-
navigationType));
174+
navigationType), NavigationMessage.Channels.Navigated);
161175
}
162176

163177
/// <summary>
@@ -167,11 +181,21 @@ protected async Task NavigateToAsync(ViewModelBase? main, ViewModelBase? sub,
167181
/// <returns>(MainViewModel, SubViewModel)</returns>
168182
public async Task<(ViewModelBase? mainVm, ViewModelBase? subVm)> GoBackAsync()
169183
{
170-
if (!TryPopHistory(out var main, out var sub))
171-
return (CurrentMainViewModel, CurrentSubViewModel);
184+
try
185+
{
186+
if (!TryPopHistory(out var main, out var sub))
187+
return (CurrentMainViewModel, CurrentSubViewModel);
188+
189+
await NavigateToAsync(main, sub, NavigationType.Backward);
190+
return (main, sub);
191+
}
192+
catch (Exception ex)
193+
{
194+
// TODO: logger
195+
Console.WriteLine($"[{nameof(NavigationService)}] navigation failed: {ex}");
196+
}
172197

173-
await NavigateToAsync(main, sub, NavigationType.Backward);
174-
return (main, sub);
198+
return (null, null);
175199
}
176200

177201
/// <summary>

PCL.Neo/ViewModels/Home/HomeSubViewModel.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ private void ManageVersions()
195195
private async Task OpenGameSettings()
196196
{
197197
// 导航到游戏设置视图
198-
_navigationService.NavigateTo(Ioc.Default.GetRequiredService<HomeViewModelBackup>(), _gameSettingsViewModel);
198+
// TODO: NavigationService 设计某个可以传特定实例的方法
199+
200+
// _navigationService.NavigateTo(Ioc.Default.GetRequiredService<HomeViewModelBackup>(), _gameSettingsViewModel);
199201

200202
// 如果版本ID不为空,初始化为当前选中的版本
201203
if (SelectedGameVersion != null)

PCL.Neo/ViewModels/HomeViewModel.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Threading.Tasks;
88
using Avalonia.Media.Imaging;
99
using Avalonia.Platform;
10+
using CommunityToolkit.Mvvm.Messaging;
11+
using PCL.Neo.Messages;
1012
using SkiaSharp;
1113
using System.IO;
1214

@@ -48,15 +50,30 @@ public HomeViewModel(INavigationService navigationService, UserService userServi
4850
// 订阅用户更改事件
4951
_userService.CurrentUserChanged += OnCurrentUserChanged;
5052

51-
// 订阅子视图模型变化
52-
// FIXME: TODO: potential memory leak
53-
_navigationService.Navigated += args => CurrentSubViewModel = args.NewSubViewModel;
54-
5553
// 初始化当前用户信息
5654
if (_userService.CurrentUser != null)
5755
{
5856
UpdateCurrentUserInfo(_userService.CurrentUser);
5957
}
58+
59+
// 启用所有 Messenger 注册事件
60+
IsActive = true;
61+
}
62+
63+
protected override void OnActivated()
64+
{
65+
base.OnActivated();
66+
67+
Messenger.Register<HomeViewModel, NavigationMessage, Guid>(
68+
this,
69+
NavigationMessage.Channels.Navigated,
70+
(_, m) => OnNavigated(m));
71+
}
72+
73+
private void OnNavigated(NavigationMessage message)
74+
{
75+
// 订阅子视图模型变化
76+
CurrentSubViewModel = message.NewSubViewModel;
6077
}
6178

6279
private void OnCurrentUserChanged(UserInfo? user)

PCL.Neo/ViewModels/MainWindowViewModel.cs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using Avalonia.Controls;
22
using CommunityToolkit.Mvvm.ComponentModel;
33
using CommunityToolkit.Mvvm.Input;
4+
using CommunityToolkit.Mvvm.Messaging;
45
using PCL.Neo.Controls.MyMsg;
56
using PCL.Neo.Services;
67
using PCL.Neo.Helpers;
8+
using PCL.Neo.Messages;
79
using System;
810
using System.Threading.Tasks;
911

@@ -62,22 +64,38 @@ public MainWindowViewModel()
6264
public MainWindowViewModel(Window window)
6365
{
6466
this._window = window;
67+
68+
// 启用所有 Messenger 注册事件
69+
IsActive = true;
6570
}
6671

6772
public MainWindowViewModel(INavigationService navigationService)
6873
{
6974
NavigationService = navigationService;
70-
NavigationService.Navigated += args =>
71-
{
72-
if (args.IsMainViewModelChanged)
73-
CurrentViewModel = args.NewMainViewModel;
74-
if (args.IsSubViewModelChanged)
75-
CurrentSubViewModel = args.NewSubViewModel;
76-
// 更新返回按钮状态
77-
CanGoBack = NavigationService.CanGoBack;
78-
// 由外部的页面跳转反向触发设置按钮状态
79-
UpdateNavBtnState();
80-
};
75+
76+
// 启用所有 Messenger 注册事件
77+
IsActive = true;
78+
}
79+
80+
protected override void OnActivated()
81+
{
82+
base.OnActivated();
83+
84+
Messenger.Register<MainWindowViewModel, NavigationMessage, Guid>(
85+
this, NavigationMessage.Channels.Navigated,
86+
(_, m) => OnNavigated(m));
87+
}
88+
89+
public void OnNavigated(NavigationMessage message)
90+
{
91+
if (message.IsMainViewModelChanged)
92+
CurrentViewModel = message.NewMainViewModel;
93+
if (message.IsSubViewModelChanged)
94+
CurrentSubViewModel = message.NewSubViewModel;
95+
// 更新返回按钮状态
96+
CanGoBack = NavigationService.CanGoBack;
97+
// 由外部的页面跳转反向触发设置按钮状态
98+
UpdateNavBtnState();
8199
}
82100

83101
[RelayCommand]

PCL.Neo/ViewModels/ViewModelBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace PCL.Neo.ViewModels;
44

5-
public partial class ViewModelBase : ObservableObject
5+
public partial class ViewModelBase : ObservableRecipient
66
{
77

88
}

0 commit comments

Comments
 (0)