Skip to content

Commit c6017dd

Browse files
committed
Add direct dialog results, nav shutdown, system nav/closed window event hooks
1 parent 8f0328c commit c6017dd

19 files changed

Lines changed: 347 additions & 216 deletions

IconPackBuilder/IconPackBuilder/AppWindow.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,10 @@ public AppWindow()
3636
};
3737

3838
Content = rootNav;
39-
_navigator = CreateNavigator(rootNav, services.BuildServiceProvider());
4039

41-
#if !WINDOWS
42-
var navManager = Windows.UI.Core.SystemNavigationManager.GetForCurrentView();
43-
navManager.AppViewBackButtonVisibility = Windows.UI.Core.AppViewBackButtonVisibility.Visible;
44-
navManager.BackRequested += (s, e) => e.Handled = _navigator.HandleSystemBackRequest();
45-
#endif
40+
_navigator = CreateNavigator(rootNav, services.BuildServiceProvider());
41+
_navigator.HookWindowClosedEvents(this);
42+
_navigator.HookSystemNavigationRequests();
4643
}
4744

4845
public async void BeginNavigation()

Playground/Playground.ViewModels/Home/HomeViewModel.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,23 @@ public partial class HomeViewModel : ObservableObject, IRoutedViewModel
88
[ObservableProperty]
99
public partial string Messages { get; private set; }
1010

11+
[ObservableProperty]
12+
public partial bool AllowNavigatingAway { get; set; }
13+
1114
public HomeViewModel(IMessageProvider messageProvider, MessageContainer messageContainer, MessageContainer2 messageContainer2)
1215
{
1316
Messages = $"IMessageProvider: {messageProvider.GetMessage()}\r\n\r\n" +
1417
$"MessageContainer: {messageContainer.Message}\r\n\r\n" +
1518
$"MessageContainer2: {messageContainer2.Message}";
1619
}
20+
21+
public async Task OnNavigatingAwayAsync(NavigatingArgs args)
22+
{
23+
if (!AllowNavigatingAway)
24+
{
25+
await Task.Delay(1000); // Simulate some async work
26+
await this.Navigator.ShowMessageDialogAsync("Navigation cancelled (AllowNavigatingAway is false).");
27+
args.Cancel = true;
28+
}
29+
}
1730
}

Playground/Playground.ViewModels/MainViewModel.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,34 @@ public partial class MainViewModel : ObservableObject, IRoutedViewModel, IMessag
2020
[ObservableProperty]
2121
public partial MenuItem SelectedMenuItem { get; set; } = new("", null);
2222

23-
partial void OnSelectedMenuItemChanged(MenuItem value)
23+
partial void OnSelectedMenuItemChanged(MenuItem oldValue, MenuItem newValue)
2424
{
25-
if (Navigator.IsNavigating || Navigator.CurrentRoute.Parts.Last() == value.ChildRoutePart)
25+
if (Navigator.IsNavigating || Navigator.CurrentRoute.Parts.Contains(newValue.ChildRoutePart))
2626
return;
2727

2828
this.TaskRunner.RunAndForget(async () => {
29-
if (value.ChildRoutePart is not null)
29+
NavigationResult result;
30+
31+
if (newValue.ChildRoutePart is not null)
32+
{
33+
result = await Navigator.NavigatePartialAsync(newValue.ChildRoutePart);
34+
}
35+
else
3036
{
31-
await Navigator.NavigatePartialAsync(value.ChildRoutePart);
32-
return;
37+
await Task.Delay(500); // Simulate logout
38+
result = await Navigator.NavigateAsync(Routes.LoginRoot);
3339
}
3440

35-
await Task.Delay(500); // Simulate logout
36-
await Navigator.NavigateAsync(Routes.LoginRoot);
41+
if (result is not NavigationResult.Success)
42+
SelectedMenuItem = oldValue;
3743
});
3844
}
3945

4046
public async Task OnNavigatedToAsync(NavigationArgs args)
4147
{
48+
this.SetChildService(new MessageContainer("Hello from MainViewModel via child service MessageContainer!"));
4249
SelectedMenuItem = MainMenuItems[0];
4350
await Task.Delay(1000);
44-
this.SetChildService(new MessageContainer("Hello from MainViewModel via child service MessageContainer!"));
4551
}
4652

4753
public Task OnRouteNavigatedAsync(NavigationArgs args)

Playground/Playground/AppWindow.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,10 @@ public AppWindow()
3030
};
3131

3232
Content = rootNav;
33-
_navigator = CreateNavigator(rootNav);
3433

35-
#if !WINDOWS
36-
var navManager = Windows.UI.Core.SystemNavigationManager.GetForCurrentView();
37-
navManager.AppViewBackButtonVisibility = Windows.UI.Core.AppViewBackButtonVisibility.Visible;
38-
navManager.BackRequested += (s, e) => e.Handled = _navigator.HandleSystemBackRequest();
39-
#endif
34+
_navigator = CreateNavigator(rootNav);
35+
_navigator.HookWindowClosedEvents(this);
36+
_navigator.HookSystemNavigationRequests();
4037
}
4138

4239
public async void BeginNavigation()

Playground/Playground/Views/Home/HomePage.xaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
77

88
<Grid>
9-
<TextBlock Text="{x:Bind Model.Messages}"
10-
HorizontalAlignment="Center"
11-
VerticalAlignment="Center"/>
9+
<StackPanel Spacing="20" HorizontalAlignment="Center" VerticalAlignment="Center">
10+
<CheckBox Content="Allow Navigating Away" IsChecked="{x:Bind Model.AllowNavigatingAway, Mode=TwoWay}"/>
11+
12+
<TextBlock Text="{x:Bind Model.Messages}" />
13+
</StackPanel>
1214
</Grid>
1315
</UserControl>

Source/Singulink.UI.Navigation.WinUI/DialogNavigator.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ internal sealed class DialogNavigator(Navigator navigator, ContentDialog dialog)
1010

1111
public ITaskRunner TaskRunner => field ??= new TaskRunner(busy => dialog.IsEnabled = !busy);
1212

13-
/// <inheritdoc cref="IDialogPresenter.ShowDialogAsync{TViewModel}(TViewModel)"/>
14-
public async Task ShowDialogAsync<TViewModel>(TViewModel viewModel)
15-
where TViewModel : class, IDialogViewModel
13+
/// <inheritdoc cref="IDialogPresenter.ShowDialogAsync(IDialogViewModel)"/>
14+
public async Task ShowDialogAsync(IDialogViewModel viewModel) => await navigator.ShowDialogAsync(dialog, viewModel);
15+
16+
/// <inheritdoc cref="IDialogPresenter.ShowDialogAsync{TResult}(IDialogViewModel{TResult})"/>
17+
public async Task<TResult> ShowDialogAsync<TResult>(IDialogViewModel<TResult> viewModel)
1618
{
1719
await navigator.ShowDialogAsync(dialog, viewModel);
20+
return viewModel.Result;
1821
}
1922

2023
/// <inheritdoc cref="IDialogNavigator.Close"/>

Source/Singulink.UI.Navigation.WinUI/MappingInfo.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal sealed class MappingInfo
1717
/// <summary>
1818
/// Gets the view model type.
1919
/// </summary>
20+
// Note: NonPublicConstructors is needed for calling GetUninitializedObject() in CreateViewModel().
2021
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
2122
public Type ViewModelType { get; init; }
2223

@@ -35,14 +36,19 @@ private MappingInfo(
3536
ViewModelType = viewModelType;
3637
ViewType = viewType;
3738

38-
_vmCtor = viewModelType.GetConstructors().OrderByDescending(c => c.GetParameters().Length).FirstOrDefault() ??
39-
throw new InvalidOperationException($"View model type '{viewModelType}' must have a public constructor.");
39+
var ctorCandidates = viewModelType.GetConstructors();
4040

41-
var nullabilityContext = new NullabilityInfoContext();
41+
if (ctorCandidates.Length is 0)
42+
throw new InvalidOperationException($"View model type '{viewModelType}' does not have any public constructors.");
4243

43-
_vmCtorParams = _vmCtor.GetParameters()
44-
.Select(p => (p, nullabilityContext.Create(p).WriteState is NullabilityState.Nullable))
45-
.ToImmutableArray();
44+
if (ctorCandidates.Length > 1)
45+
throw new InvalidOperationException($"View model type '{viewModelType}' has multiple public constructors.");
46+
47+
_vmCtor = ctorCandidates[0];
48+
_vmCtorParams = [.. _vmCtor.GetParameters().Select(p => (p, new NullabilityInfoContext().Create(p).WriteState is NullabilityState.Nullable))];
49+
50+
if (_vmCtorParams.Any(p => p.Param.ParameterType.IsByRef))
51+
throw new InvalidOperationException($"View model type '{viewModelType}' has a constructor with by-ref parameters, which is not supported.");
4652
}
4753

4854
/// <summary>
@@ -53,7 +59,9 @@ public static MappingInfo Create(
5359
Type viewModelType,
5460
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
5561
Type viewType)
56-
=> new(viewModelType, viewType);
62+
{
63+
return new(viewModelType, viewType);
64+
}
5765

5866
/// <summary>
5967
/// Creates an instance of the view type.
@@ -80,14 +88,19 @@ public IRoutedViewModelBase CreateViewModel(INavigator navigator, ConcreteRoute.
8088
var (param, isNullable) = _vmCtorParams[i];
8189
var paramType = param.ParameterType;
8290

83-
args[i] = routeItem.ParentItem?.GetChildViewModelService(paramType) ?? navigator.Services.GetService(paramType);
91+
args[i] = routeItem.ParentItem?.GetChildViewModelService(paramType) ?? navigator.RootServices.GetService(paramType);
8492

8593
if (args[i] is null)
8694
{
8795
if (param.HasDefaultValue)
96+
{
8897
args[i] = param.DefaultValue;
98+
}
8999
else if (!isNullable)
90-
throw new InvalidOperationException($"Cannot resolve required constructor parameter '{param.Name}' of type '{paramType}' for view model type '{ViewModelType}'.");
100+
{
101+
throw new InvalidOperationException(
102+
$"Cannot resolve required constructor parameter '{param.Name}' of type '{paramType}' for view model type '{ViewModelType}'.");
103+
}
91104
}
92105
}
93106

Source/Singulink.UI.Navigation.WinUI/Navigator.Dialogs.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,27 @@ namespace Singulink.UI.Navigation.WinUI;
55
/// <content>
66
/// Provides dialog related implementations for the navigator.
77
/// </content>
8-
partial class Navigator
8+
partial class Navigator : IDialogPresenter
99
{
10-
/// <inheritdoc cref="IDialogPresenter.ShowDialogAsync{TViewModel}(TViewModel)"/>
11-
public async Task ShowDialogAsync<TViewModel>(TViewModel viewModel) where TViewModel : class, IDialogViewModel
10+
/// <inheritdoc cref="IDialogPresenter.ShowDialogAsync(IDialogViewModel)"/>
11+
public async Task ShowDialogAsync(IDialogViewModel viewModel)
1212
{
1313
await ShowDialogAsync(null, viewModel);
1414
}
1515

16-
internal async Task ShowDialogAsync<TViewModel>(ContentDialog? requestingParentDialog, TViewModel viewModel) where TViewModel : class, IDialogViewModel
16+
/// <inheritdoc cref="IDialogPresenter.ShowDialogAsync{TResult}(IDialogViewModel{TResult})"/>
17+
public async Task<TResult> ShowDialogAsync<TResult>(IDialogViewModel<TResult> viewModel)
18+
{
19+
await ShowDialogAsync(null, viewModel);
20+
return viewModel.Result;
21+
}
22+
23+
internal async Task ShowDialogAsync(ContentDialog? requestingParentDialog, IDialogViewModel viewModel)
1724
{
1825
EnsureThreadAccess();
1926

2027
if (_blockDialogs)
21-
throw new InvalidOperationException("Show dialog requested at an invalid time while dialogs are blocked.");
28+
throw new InvalidOperationException("Show dialog requested at an invalid time while showing dialogs is blocked.");
2229

2330
EnsureDialogIsTopDialog(requestingParentDialog);
2431
CloseLightDismissPopups();
@@ -76,11 +83,10 @@ internal void CloseDialog(ContentDialog dialog)
7683
dialogInfo.Tcs.SetResult();
7784
}
7885

79-
private ContentDialog CreateDialogFor<TViewModel>(TViewModel viewModel)
80-
where TViewModel : class, IDialogViewModel
86+
private ContentDialog CreateDialogFor(IDialogViewModel viewModel)
8187
{
82-
if (!_viewModelTypeToDialogActivator.TryGetValue(typeof(TViewModel), out var ctorFunc))
83-
throw new KeyNotFoundException($"No dialog registered for view model of type '{typeof(TViewModel)}'.");
88+
if (!_viewModelTypeToDialogActivator.TryGetValue(viewModel.GetType(), out var ctorFunc))
89+
throw new KeyNotFoundException($"No dialog registered for view model of type '{viewModel.GetType()}'.");
8490

8591
var dialog = ctorFunc.Invoke();
8692
dialog.DataContext = viewModel;
@@ -140,11 +146,8 @@ async void OnDialogClosing(ContentDialog sender, ContentDialogClosingEventArgs a
140146

141147
// Make sure we are still the top dialog and another event didn't close the dialog after the yield.
142148

143-
if (_dialogStack.TryPeek(out dialogInfo) && dialogInfo.Dialog == sender &&
144-
MixinManager.GetNavigator(dismissibleVm) is { } navigator && !navigator.TaskRunner.IsBusy)
145-
{
146-
await navigator.TaskRunner.RunAsBusyAsync(dismissibleVm.OnDismissRequestedAsync());
147-
}
149+
if (_dialogStack.TryPeek(out dialogInfo) && dialogInfo.Dialog == sender && !dismissibleVm.TaskRunner.IsBusy)
150+
await dismissibleVm.TaskRunner.RunAsBusyAsync(dismissibleVm.OnDismissRequestedAsync());
148151
}
149152
}
150153
}

0 commit comments

Comments
 (0)