Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solve concurrency issues #31

Merged
merged 1 commit into from
May 6, 2024
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
2 changes: 1 addition & 1 deletion Samples/Nalu.Maui.Sample/PageModels/FourPageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public partial class FourPageModel(INavigationService navigationService) : Obser
private Task PopToOneAsync() => navigationService.GoToAsync(Navigation.Absolute(NavigationBehavior.IgnoreGuards).ShellContent<OnePage>());

[RelayCommand(AllowConcurrentExecutions = false)]
private Task NavigateToTwoAsync() => navigationService.GoToAsync(Navigation.Relative());
private Task NavigateToTwoAsync() => navigationService.GoToAsync(Navigation.Absolute(NavigationBehavior.IgnoreGuards).ShellContent<TwoPageModel>());

[RelayCommand(AllowConcurrentExecutions = false)]
private Task NavigateToFiveAsync() => navigationService.GoToAsync(Navigation.Absolute(NavigationBehavior.PopAllPagesOnItemChange | NavigationBehavior.IgnoreGuards).ShellContent<FivePageModel>());
Expand Down
5 changes: 4 additions & 1 deletion Samples/Nalu.Maui.Sample/PageModels/TwoPageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ namespace Nalu.Maui.Sample.PageModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class TwoPageModel(INavigationService navigationService) : ObservableObject
public partial class TwoPageModel(INavigationService navigationService) : ObservableObject, IAppearingAware
{
private static int _instanceCount;

public int InstanceCount { get; } = Interlocked.Increment(ref _instanceCount);

[RelayCommand(AllowConcurrentExecutions = false)]
private Task PushSixAsync() => navigationService.GoToAsync(Navigation.Relative().Push<SixPageModel>());

// Simulate long loading times on the page
public async ValueTask OnAppearingAsync() => await Task.Delay(500).ConfigureAwait(true);
}
57 changes: 37 additions & 20 deletions Source/Nalu.Maui.Navigation/Internals/NavigationService.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
namespace Nalu;

using System.Diagnostics;
using System.Runtime.CompilerServices;

#pragma warning disable IDE0290

internal class NavigationService : INavigationService, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly AsyncLocal<StrongBox<bool>> _isNavigating = new();
private readonly LeakDetector _leakDetector;
private IShellProxy? _shellProxy;

Expand Down Expand Up @@ -60,30 +62,30 @@ public async Task<bool> GoToAsync(INavigationInfo navigation)

var disposeBag = new HashSet<object>();
var shellProxy = ShellProxy;
shellProxy.BeginNavigation();

try
return await ExecuteNavigationAsync(async () =>
{
return await ExecuteNavigationAsync(() =>
shellProxy.BeginNavigation();
try
{
var ignoreGuards = navigation.Behavior?.HasFlag(NavigationBehavior.IgnoreGuards) ?? false;
return navigation switch
return await (navigation switch
{
{ IsAbsolute: true } => ExecuteAbsoluteNavigationAsync(navigation, disposeBag, ignoreGuards),
_ => ExecuteRelativeNavigationAsync(navigation, disposeBag, ignoreGuards: ignoreGuards),
};
}).ConfigureAwait(true);
}
finally
{
await shellProxy.CommitNavigationAsync(() =>
{ IsAbsolute: true } => ExecuteAbsoluteNavigationAsync(navigation, disposeBag, ignoreGuards).ConfigureAwait(true),
_ => ExecuteRelativeNavigationAsync(navigation, disposeBag, ignoreGuards: ignoreGuards).ConfigureAwait(true),
});
}
finally
{
foreach (var toDispose in disposeBag)
await shellProxy.CommitNavigationAsync(() =>
{
DisposePage(toDispose);
}
}).ConfigureAwait(true);
}
foreach (var toDispose in disposeBag)
{
DisposePage(toDispose);
}
}).ConfigureAwait(true);
}
}).ConfigureAwait(true);
}

internal Page CreatePage(Type pageType, Page? parentPage)
Expand Down Expand Up @@ -465,12 +467,25 @@ private RelativeNavigation ToRelativeNavigation(INavigationInfo navigation, ILis

private async Task<bool> ExecuteNavigationAsync(Func<Task<bool>> navigationFunc)
{
var taken = await _semaphore.WaitAsync(0).ConfigureAwait(true);
if (!taken)
if (_isNavigating.Value is { Value: true })
{
throw new InvalidNavigationException("Cannot trigger a navigation from within a navigation, try to use IDispatcher.DispatchDelayed.");
}

var shellProxy = ShellProxy;
var initialState = shellProxy.State;

await _semaphore.WaitAsync().ConfigureAwait(true);

if (initialState != shellProxy.State)
{
throw new InvalidNavigationException("Cannot navigate while another navigation is in progress, try to use IDispatcher.DispatchDelayed.");
// State has changed, abort the navigation
_semaphore.Release();
return false;
}

_isNavigating.Value = new StrongBox<bool>(true);

try
{
var result = await navigationFunc().ConfigureAwait(true);
Expand All @@ -479,6 +494,8 @@ private async Task<bool> ExecuteNavigationAsync(Func<Task<bool>> navigationFunc)
finally
{
_semaphore.Release();

_isNavigating.Value.Value = false;
}
}

Expand Down
30 changes: 16 additions & 14 deletions Source/Nalu.Maui.Navigation/NaluShell.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Nalu;

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

#pragma warning disable IDE0290
Expand All @@ -14,8 +15,7 @@ public abstract partial class NaluShell : Shell, INaluShell, IDisposable
private readonly NavigationService _navigationService;
private readonly object? _rootPageIntent;
private readonly string _rootPageRoute;
private readonly object _lock = new();
private bool _isNavigating;
private readonly AsyncLocal<StrongBox<bool>> _isNavigating = new();
private bool _initialized;
private ShellProxy? _shellProxy;

Expand Down Expand Up @@ -61,10 +61,13 @@ public void Dispose()

internal void SetIsNavigating(bool value)
{
lock (_lock)
if (_isNavigating.Value is { } isNavigating)
{
_isNavigating = value;
isNavigating.Value = value;
return;
}

_isNavigating.Value ??= new StrongBox<bool>(value);
}

/// <summary>
Expand Down Expand Up @@ -119,9 +122,11 @@ protected override async void OnNavigating(ShellNavigatingEventArgs args)

// Only reason we're here is due to shell content navigation from Shell Flyout or Tab bars
// Now find the ShellContent target and navigate to it via the navigation service
var segments = uri.Split('/', StringSplitOptions.RemoveEmptyEntries);
var shellContentSegmentName = NormalizeSegmentRegex().Replace(segments.Length > 3 ? segments[2] : segments[^1], string.Empty);
var shellContent = (ShellContentProxy)_shellProxy!.GetContent(shellContentSegmentName);
var segments = uri
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(NormalizeSegment)
.ToArray();
var shellContent = (ShellContentProxy)_shellProxy!.FindContent(segments);
var shellSection = shellContent.Parent;

var ownsNavigationStack = shellSection.CurrentContent == shellContent;
Expand Down Expand Up @@ -165,14 +170,11 @@ protected override async void OnNavigating(ShellNavigatingEventArgs args)
}
}

private bool GetIsNavigating()
{
lock (_lock)
{
return _isNavigating;
}
}
private bool GetIsNavigating() => _isNavigating.Value?.Value ?? false;

private static readonly Regex _normalizeSegmentRegex = NormalizeSegmentRegex();
[GeneratedRegex("^(D_FAULT_|IMPL_)")]
private static partial Regex NormalizeSegmentRegex();
private static string NormalizeSegment(string segment)
=> _normalizeSegmentRegex.Replace(segment, string.Empty);
}
1 change: 1 addition & 0 deletions Source/Nalu.Maui.Navigation/ShellInfo/IShellProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace Nalu;

internal interface IShellProxy
{
string State { get; }
bool BeginNavigation();
Task CommitNavigationAsync(Action? completeAction = null);
IShellItemProxy CurrentItem { get; }
Expand Down
17 changes: 17 additions & 0 deletions Source/Nalu.Maui.Navigation/ShellInfo/ShellProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal class ShellProxy : IShellProxy, IDisposable
private string? _navigationTarget;
private bool _contentChanged;
private IShellSectionProxy? _navigationCurrentSection;
public string State => _shell.CurrentState.Location.OriginalString;

public ShellProxy(NaluShell shell)
{
Expand Down Expand Up @@ -79,6 +80,22 @@ public async Task CommitNavigationAsync(Action? completeAction = null)

public IShellContentProxy GetContent(string segmentName) => _contentsBySegmentName[segmentName];

public IShellContentProxy FindContent(params string[] names)
{
var namesLength = names.Length;
var name = names[0];
for (var i = 0; i < namesLength; i++)
{
name = names[i];
if (_contentsBySegmentName.TryGetValue(name, out var content))
{
return content;
}
}

throw new KeyNotFoundException($"Could not find content with segment name '{name}'");
}

public Color GetToolbarIconColor(Page page) => Shell.GetTitleColor(page.IsSet(Shell.TitleColorProperty) ? page : _shell);

public async Task PushAsync(string segmentName, Page page)
Expand Down