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

Advanced startup navigation #25

Merged
merged 1 commit into from
Oct 12, 2023
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
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,46 @@ navigationService.Navigate(navigationUri);
```

## Choosing the start page of your app
### (navigationService, serviceProvider)

In the below example, we use both an `INavigationService` and an `IServiceProvider`. The `IServiceProvider` is used to resolve the .NET MAUI service, [`IPreferences`](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/storage/preferences?tabs=android). If a username is stored in preferences, we use the `INavigationService` to go to the `HomePage` of the app. Otherwise, we go to the `LoginPage`.

``` csharp
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseBurkusMvvm(burkusMvvm =>
{
burkusMvvm.OnStart(async (navigationService, serviceProvider) =>
{
var preferences = serviceProvider.GetRequiredService<IPreferences>();

if (preferences.ContainsKey(PreferenceKeys.Username))
{
// we are logged in to the app
await navigationService.Push<HomePage>();
}
else
{
// logged out so we need to get the user to login
await navigationService.Push<LoginPage>();
}
});
})
...
```
### (IServiceProvider serviceProvider)

It is possible to have a service that decides which page is most appropriate to navigate to. This service could decide to:
- Navigate to the "Terms & Conditions" page if the user has not agreed to the latest terms yet
- Navigate to the "Signup / Login" page if the user is logged out
- Navigate to the "Home" page if the user has used the app before and doesn't need to do anything

In the below example, we only resolve a `IServiceProvider` which allows us to resolve `IAppStartupService`. The `IAppStartupService` will call the `INavigationService` internally to do the navigation.
```csharp
public static class MauiProgram
{
Expand All @@ -238,9 +274,9 @@ public static class MauiProgram
.UseMauiApp<App>()
.UseBurkusMvvm(burkusMvvm =>
{
burkusMvvm.OnStart(async (navigationService) =>
burkusMvvm.OnStart(async (IServiceProvider serviceProvider) =>
{
var appStartupService = ServiceResolver.Resolve<IAppStartupService>();
var appStartupService = serviceProvider.GetRequiredService<IAppStartupService>();
await appStartupService.NavigateToFirstPage();
});
})
Expand Down
16 changes: 14 additions & 2 deletions samples/DemoApp/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DemoApp.Abstractions;
using DemoApp.Models;
using DemoApp.Services;
using DemoApp.ViewModels;
using DemoApp.Views;
Expand All @@ -15,9 +16,19 @@ public static MauiApp CreateMauiApp()
.UseMauiApp<App>()
.UseBurkusMvvm(burkusMvvm =>
{
burkusMvvm.OnStart(async (navigationService) =>
burkusMvvm.OnStart(async (navigationService, serviceProvider) =>
{
await navigationService.Push<LoginPage>();
var preferences = serviceProvider.GetRequiredService<IPreferences>();

if (preferences.ContainsKey(PreferenceKeys.Username))
{
// we are logged in to the app
await navigationService.Push<HomePage>();
}
else
{
await navigationService.Push<LoginPage>();
}
});
})
.RegisterViewModels()
Expand Down Expand Up @@ -62,6 +73,7 @@ public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)

public static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddSingleton(Preferences.Default);
mauiAppBuilder.Services.AddSingleton<IWeatherService, WeatherService>();

return mauiAppBuilder;
Expand Down
6 changes: 6 additions & 0 deletions samples/DemoApp/Models/NavigationParameterKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace DemoApp.Models;

public static class NavigationParameterKeys
{
public static readonly string Username = "username";
}
10 changes: 10 additions & 0 deletions samples/DemoApp/Models/PreferenceKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace DemoApp.Models;

public static class PreferenceKeys
{
#region Preference keys

public static readonly string Username = "username";

#endregion Preference keys
}
32 changes: 27 additions & 5 deletions samples/DemoApp/ViewModels/ChangeUsernameViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Burkus.Mvvm.Maui;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DemoApp.Models;
using DemoApp.Properties;

namespace DemoApp.ViewModels;

public partial class ChangeUsernameViewModel : BaseViewModel
{
#region Fields

private IPreferences preferences { get; }

private bool wasAnimatedNavigationUsed;

#endregion Fields
Expand All @@ -21,9 +26,11 @@ public partial class ChangeUsernameViewModel : BaseViewModel
#region Constructors

public ChangeUsernameViewModel(
INavigationService navigationService)
INavigationService navigationService,
IPreferences preferences)
: base(navigationService)
{
this.preferences = preferences;
}

#endregion Constructors
Expand All @@ -43,9 +50,15 @@ public override async Task OnNavigatingFrom(NavigationParameters parameters)
{
await base.OnNavigatingFrom(parameters);

// pass 'Username' back regardless if the user presses the button
// or uses a different method of closing the modal (e.g. Android back button)
parameters.Add("username", Username);
if (IsValidUsername())
{
// save username as a preference
preferences.Set(PreferenceKeys.Username, Username);

// pass 'Username' back regardless if the user presses the button
// or uses a different method of closing the modal (e.g. Android back button)
parameters.Add(NavigationParameterKeys.Username, Username);
}

// this is a modal, so we need to close it modally
parameters.UseModalNavigation = true;
Expand All @@ -70,4 +83,13 @@ private async Task Finish()
}

#endregion Commands

#region Private methods

private bool IsValidUsername()
{
return !string.IsNullOrWhiteSpace(Username);
}

#endregion Private methods
}
21 changes: 16 additions & 5 deletions samples/DemoApp/ViewModels/HomeViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DemoApp.Abstractions;
using DemoApp.Models;
using DemoApp.Views;

namespace DemoApp.ViewModels;
Expand All @@ -9,7 +10,9 @@ public partial class HomeViewModel : BaseViewModel
{
#region Fields

protected IWeatherService weatherService { get; }
private IPreferences preferences { get; }

private IWeatherService weatherService { get; }

#endregion Fields

Expand All @@ -27,9 +30,11 @@ public partial class HomeViewModel : BaseViewModel

public HomeViewModel(
INavigationService navigationService,
IPreferences preferences,
IWeatherService weatherService)
: base(navigationService)
{
this.preferences = preferences;
this.weatherService = weatherService;
}

Expand All @@ -41,11 +46,14 @@ public override async Task OnNavigatedTo(NavigationParameters parameters)
{
await base.OnNavigatedTo(parameters);

var usernameValue = parameters.GetValue<string>("username");

if (!string.IsNullOrWhiteSpace(usernameValue))
if (parameters.ContainsKey(NavigationParameterKeys.Username))
{
Username = usernameValue;
Username = parameters.GetValue<string>(NavigationParameterKeys.Username);
}
else
{
// load the username from preferences
Username = preferences.Get<string>(PreferenceKeys.Username, default);
}

CurrentWeatherDescription = weatherService.GetWeatherDescription();
Expand Down Expand Up @@ -95,6 +103,9 @@ private async Task GoToTabbedPageDemo()
[RelayCommand]
private async Task Logout()
{
// remove the username preference
preferences.Remove(PreferenceKeys.Username);

// use the navigate URI syntax to logout with an absolute URI
await navigationService.Navigate("/LoginPage");
}
Expand Down
16 changes: 12 additions & 4 deletions samples/DemoApp/ViewModels/LoginViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DemoApp.Models;
using DemoApp.Properties;
using DemoApp.Views;

Expand All @@ -9,7 +10,9 @@ public partial class LoginViewModel : BaseViewModel
{
#region Fields

protected IDialogService dialogService { get; }
private IDialogService dialogService { get; }

private IPreferences preferences { get; }

#endregion Fields

Expand All @@ -27,10 +30,12 @@ public partial class LoginViewModel : BaseViewModel

public LoginViewModel(
IDialogService dialogService,
INavigationService navigationService)
INavigationService navigationService,
IPreferences preferences)
: base(navigationService)
{
this.dialogService = dialogService;
this.preferences = preferences;
}

#endregion Constructors
Expand All @@ -49,9 +54,12 @@ private async Task Login()
return;
}

// save username as a preference
preferences.Set(PreferenceKeys.Username, Username);

var navigationParameters = new NavigationParameters
{
{ "username", Username },
{ NavigationParameterKeys.Username, Username },
};

// after we login, we replace the stack so the user can't go back to the Login page
Expand All @@ -73,7 +81,7 @@ private async Task Register()

private bool IsValidLoginForm()
{
if (string.IsNullOrEmpty(Username))
if (string.IsNullOrWhiteSpace(Username))
{
dialogService.DisplayAlert(
Resources.Error,
Expand Down
2 changes: 1 addition & 1 deletion src/Abstractions/IBurkusMvvmBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

internal interface IBurkusMvvmBuilder
{
Func<INavigationService, Task> onStartFunc { get; set; }
Func<INavigationService, IServiceProvider, Task> onStartFunc { get; set; }
}
2 changes: 1 addition & 1 deletion src/Builders/InternalBurkusMvvmBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
/// </summary>
internal class InternalBurkusMvvmBuilder : BurkusMvvmBuilder, IBurkusMvvmBuilder
{
public Func<INavigationService, Task> onStartFunc { get; set; }
public Func<INavigationService, IServiceProvider, Task> onStartFunc { get; set; }

Check warning on line 8 in src/Builders/InternalBurkusMvvmBuilder.cs

View workflow job for this annotation

GitHub Actions / build-plugin-ci

Non-nullable property 'onStartFunc' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
26 changes: 24 additions & 2 deletions src/Extensions/BurkusMvvmBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ public static class BurkusMvvmBuilderExtensions
/// Define where the app should go first when starting. You must navigate to a page when starting.
/// </summary>
/// <param name="builder">BurkusMvvmBuilder</param>
/// <param name="onStartFunc">Function to perform when starting with access to the <see cref="INavigationService"/></param>
/// <param name="onStartFunc">Function to perform when starting with access to <see cref="INavigationService"/> and <see cref="IServiceProvider"/></param>
/// <returns></returns>
public static BurkusMvvmBuilder OnStart(this BurkusMvvmBuilder builder, Func<INavigationService, Task> onStartFunc)
public static BurkusMvvmBuilder OnStart(this BurkusMvvmBuilder builder, Func<INavigationService, IServiceProvider, Task> onStartFunc)
{
var internalBuilder = builder as InternalBurkusMvvmBuilder;

Expand All @@ -19,4 +19,26 @@ public static BurkusMvvmBuilder OnStart(this BurkusMvvmBuilder builder, Func<INa

return builder;
}

/// <summary>
/// Define where the app should go first when starting. You must navigate to a page when starting.
/// </summary>
/// <param name="builder">BurkusMvvmBuilder</param>
/// <param name="onStartFunc">Function to perform when starting with access to <see cref="INavigationService"/>.</param>
/// <returns></returns>
public static BurkusMvvmBuilder OnStart(this BurkusMvvmBuilder builder, Func<INavigationService, Task> onStartFunc)
{
return OnStart(builder, (nav, sp) => onStartFunc(nav));
}

/// <summary>
/// Define where the app should go first when starting. You must navigate to a page when starting.
/// </summary>
/// <param name="builder">BurkusMvvmBuilder</param>
/// <param name="onStartFunc">Function to perform when starting with access to <see cref="IServiceProvider"/>.</param>
/// <returns></returns>
public static BurkusMvvmBuilder OnStart(this BurkusMvvmBuilder builder, Func<IServiceProvider, Task> onStartFunc)
{
return OnStart(builder, (nav, sp) => onStartFunc(sp));
}
}
3 changes: 2 additions & 1 deletion src/Models/BurkusMvvmApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
{
protected override Window CreateWindow(IActivationState? activationState)
{
Current.MainPage = new NavigationPage();

Check warning on line 7 in src/Models/BurkusMvvmApplication.cs

View workflow job for this annotation

GitHub Actions / build-plugin-ci

Dereference of a possibly null reference.

var burkusMvvmBuilder = ServiceResolver.Resolve<IBurkusMvvmBuilder>();
var navigationService = ServiceResolver.Resolve<INavigationService>();
var serviceProvider = ServiceResolver.GetServiceProvider();

// perform the user's desired initialization logic
if (burkusMvvmBuilder.onStartFunc != null)
{
burkusMvvmBuilder.onStartFunc.Invoke(navigationService);
burkusMvvmBuilder.onStartFunc.Invoke(navigationService, serviceProvider);
}

return base.CreateWindow(activationState);
Expand Down
Loading
Loading