Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/nalu-development/nalu
Provides .NET MAUI packages to help with everyday challenges
https://github.com/nalu-development/nalu
maui
Last synced: 3 months ago
JSON representation
Provides .NET MAUI packages to help with everyday challenges
- Host: GitHub
- URL: https://github.com/nalu-development/nalu
- Owner: nalu-development
- License: mit
- Created: 2024-01-17T11:50:27.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2024-05-22T15:07:39.000Z (6 months ago)
- Last Synced: 2024-05-22T16:30:05.044Z (6 months ago)
- Topics: maui
- Language: C#
- Homepage: https://nalu-development.github.io/nalu/
- Size: 1.06 MB
- Stars: 30
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: .github/CONTRIBUTING.md
- License: LICENSE.md
- Code of conduct: .github/CODE_OF_CONDUCT.md
- Security: .github/SECURITY.md
Awesome Lists containing this project
- awesome-dotnet-maui - Nalu.Maui.Navigation - based navigation abstraction which handles `IDisposable`, provides navigation guards, and simplifies passing parameters.|[![GitHub stars](https://img.shields.io/github/stars/nalu-development/nalu?style=flat-square)](https://github.com/nalu-development/nalu/stargazers)|[![GitHub last-commit](https://img.shields.io/github/last-commit/nalu-development/nalu?style=flat-square)](https://github.com/nalu-development/nalu/commits) (UI)
README
![Banner](https://raw.githubusercontent.com/nalu-development/nalu/main/Images/Banner.png)
# Nalu [![GitHub Actions Status](https://github.com/nalu-development/nalu/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/nalu-development/nalu/actions/workflows/build.yml)
## Nalu.Maui
Nalu.Maui is a library that solves some problems like navigation between pages in a MAUI application.
### Navigation [![Nalu.Maui.Navigation NuGet Package](https://img.shields.io/nuget/v/Nalu.Maui.Navigation.svg)](https://www.nuget.org/packages/Nalu.Maui.Navigation/) [![Nalu.Maui NuGet Package Downloads](https://img.shields.io/nuget/dt/Nalu.Maui.Navigation)](https://www.nuget.org/packages/Nalu.Maui.Navigation/)
Shell-based navigation abstraction which handles `IDisposable`, provides navigation guards, and simplifies passing parameters.
Make sure to install `Nalu.Maui.Navigation` (and not just `Nalu.Maui`) package into the main MAUI project in order to avoid default menu and back icons loading issues.
#### Migration from v2.x to v3.x
Migration from `Nalu.Maui` v2.x to v3.x is not automatic, you need to update your code to use the new `Nalu.Maui.Navigation` package.
```
xmlns:nalu="https://nalu-development.github.com/nalu"
```
becomes
```
xmlns:nalu="https://nalu-development.github.com/nalu/navigation"
```#### Why Nalu navigation?
Unfortunately MAUI navigation (NavigationPage, or Shell) do not provide automatic page/view model disposal as [widely explained in this issue](https://github.com/dotnet/maui/issues/7354).
This is a problem because it can lead to memory/event leaks.There are other big issues with Shell navigation:
- `Shell.Current.GoToAsync` API is really hard to understand: can you easily tell what's the difference between `GoToAsync("Page1")` / `GoToAsync("/Page1")` / `GoToAsync("//Page1")` / `GoToAsync("///Page1")`?
- Root pages (defined as `ShellContent` will never be dispose, even if you navigate to a different shell item.
- Have you ever wonder what's the difference between `Transient` and `Scoped` service lifetime in MAUI?
- The way to pass parameters is a bit inconvenient
- There's no way to define something and provide that value to all nested pages (like a context)On the other hand, `Shell` offers a convenient way to define the app structure including tab bar and flyout menu.
`Shell` also supports having multiple navigation stacks alive at the same time when using a global `TabBar`.Nalu navigation is based on `Shell` navigation, but it solves all the issues above.
#### Dependency injection the right way
With Nalu navigation, a `ServiceScope` is created for each page, so you can use `Scoped` services in your pages and view models.
Pages and view models are in fact registered as `Scoped` services and automatically disposed by the `ServiceScope` when the page is removed from the navigation stack.#### Initial setup
First of all, you need to add the Nalu.Maui package to your project, then just call `UseNaluNavigation` in your `MauiProgram`:
```csharp
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp()
.UseNaluNavigation()
```This method will scan the `` assembly for pages and view models by naming convention `MainPage` => `MainPageModel`.
You can specify a custom naming convention by passing a function that returns the view model type given the page type:```csharp
builder
.UseMauiApp()
.UseNaluNavigation(nav => nav.AddPages((pageType) => pageType.Name.Replace("Page", "ViewModel")))
```**Important notes**:
- page models needs to implement `INotifyPropertyChanged` interface
- pages need to require the view model as constructor parameter and assign it to the `BindingContext` property```csharp
public partial class MainPage : ContentPage
{
public MainPage(MainPageModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
```Eventually you can specify the pages and view models manually:
```csharp
builder
.UseMauiApp()
.UseNaluNavigation(nav => nav.AddPage())
```To help with testability you can also register the page model as an interface:
```csharp
builder
.UseMauiApp()
.UseNaluNavigation(nav => nav.AddPage())
```Note: the automatic registration by naming convention automatically considers the page model as an interface.
#### Initial setup - without MVVM pattern
Nalu can be used even without MVVM pattern, just add your pages as `Scoped` services:
```csharp
builder
.UseNaluNavigation()
.Services
.AddScoped();
```#### Customizing the appearance of the navigation bar
Due to some issues in MAUI, we need to define `ImageSource` for the menu button and back button displayed in the navigation bar.
Nalu navigation already provides them, but you can override them if you want:```csharp
builder
.UseMauiApp()
.UseNaluNavigation(nav => nav.AddPages()
.WithMenuIcon(ImageSource.FromFile("menu.png"))
.WithBackIcon(ImageSource.FromFile("back.png")))
```#### Shell definition
Nalu navigation is based on `Shell` navigation, so you need to define your `Shell` in `AppShell.xaml` by inheriting from `NaluShell`.
Use `nalu:Navigation.PageType` to specify the page type for each `ShellContent`.```xml
```
In the code behind you need to set the initial shell page passing the navigation service and the initial page type to the base constructor:
```csharp
public partial class App : Application
{
public App(INavigationService navigationService)
{
InitializeComponent();
MainPage = new AppShell(navigationService);
}
}public partial class AppShell : NaluShell
{
public AppShell(INavigationService navigationService) : base(navigationService, typeof(OnePage))
{
InitializeComponent();
}
}
```#### Navigation concepts
`Shell` structure is based on `Item` > `Section` > `Content` hierarchy.
Even when you don't specify an `Item` or a `Section` in the `Shell` definition, it is automatically created for you.For example, the following `Shell` definition
```xml
```
is equivalent to
```xml
```
That said, `Nalu` navigation provides the following navigation behavior when switching between `ShellContent`s:
- if the content is in the same `ShellSection`, navigation stack will be popped
- if the content is in a different `ShellSection` but in the same `ShellItem`, the current navigation stack will be persisted
- if the content is in a different `ShellItem`, all of the current item's navigation stacks will be popped and the `ShellContent` pages will be destroyedYou can customize this behavior by providing a custom `NavigationBehavior` to the `Navigation` object.
For example you can also use the `IgnoreGuards` behavior to ignore the `ILeavingGuard` when popping a page:
```csharp
await _navigationService.GoToAsync(Navigation.Relative(NavigationBehavior.IgnoreGuards).Pop());
```#### Navigation events
The page view model can selectively react to navigation events by implementing the following interfaces:
- `IEnteringAware`: defines a `ValueTask OnEnteringAsync()` called when the page is entering the navigation stack
- `IAppearingAware`: defines a `ValueTask OnAppearingAsync()` called when the page is appearing
- `IDisappearingAware`: defines a `ValueTask OnDisappearingAsync()` called when the page is disappearing
- `ILeavingAware`: defines a `ValueTask OnLeavingAsync()` called when the page is leaving the navigation stackWith Nalu navigation you can also pass parameters to the target page using the `IntentAware` interfaces:
- `IEnteringAware`: defines a `ValueTask OnEnteringAsync(TIntent intent)` called when the page is entering the navigation stack
- `IAppearingAware`: defines a `ValueTask OnAppearingAsync(TIntent intent)` called when the page is appearingNote: when an intent is passed to the view model, the `OnEnteringAsync` and `OnAppearingAsync` parameterless methods will not be called.
Obviously you can call them manually from the intent-aware one if you need to.Sometimes you want to protect a page from being popped from the navigation stack, for example when the user is editing a form.
You can do that by implementing the `ILeavingGuard` interface which defines a `ValueTask CanLeaveAsync()` method from which you can eventually display a prompt to ask the user if they want to leave the page.```csharp
public class ViewModel : ILeavingGuard
{
public async ValueTask CanLeaveAsync()
{
return await ConfirmUserLeaveAsync("Are you sure you want to leave without saving?") // a method to verify the leave action
}
}
```Note: a page "appears" only when it is the target of the navigation, intermediate pages models will trigger `OnAppearingAsync` unless the `ILeavingGuard` needs to be evaluated.
#### Navigation using C#
First of all, you need to inject the `INavigationService` in your page model:
```csharp
public class OnePageModel : IOnePageModel
{
private readonly INavigationService _navigationService;public OnePageModel(INavigationService navigationService)
{
_navigationService = navigationService;
}
}
```Then you can use the `GoToAsync` method to navigate to a page using relative or absolute navigation:
```csharp
// Add a page to the navigation stack
await _navigationService.GoToAsync(Navigation.Relative().Push());
// Add a page to the navigation stack providing an intent
var myIntent = new MyIntent(/* ... */);
await _navigationService.GoToAsync(Navigation.Relative().Push().WithIntent(myIntent));
// Remove the current page from the navigation stack
await _navigationService.GoToAsync(Navigation.Relative().Pop());
// Remove the current page from the navigation stack providing an intent to the previous page
var myIntent = new MyResult(/* ... */);
await _navigationService.GoToAsync(Navigation.Relative().Pop().WithIntent(myIntent))
// Pop two pages than push a new one
await _navigationService.GoToAsync(Navigation.Relative().Pop().Pop().Push());
// Pop to the root page using absolute navigation
await _navigationService.GoToAsync(Navigation.Absolute().ShellContent());
// Switch to a different shell content and push a page there
await _navigationService.GoToAsync(Navigation.Absolute().ShellContent().Push());
```Note:
- if you don't want to use MVVM pattern just use page types instead of page model types (i.e. `Navigation.Relative().Push()`).
- non-sense navigations will throw an exception, for example: pop -> push -> pop.#### Navigation using XAML
Nalu provides a `Navigation` markup extension that can be used to navigate to a page using relative or absolute navigation:
```xml
```
```xml
```
```xml
```
#### Advanced scenario: navigation-scoped services
Sometimes you need to share a service between pages, starting from a specific page down to all the nested pages.
Nalu navigation provides an `INavigationServiceProvider` service that can be used to provide services to nested page.
In the page model where you want to start providing the service, you need to inject the `INavigationServiceProvider` and call the `Provide` method:
```csharp
public class PersonPageModel(INavigationServiceProvider navigationServiceProvider) : IPersonPageModel // which inherits from IEnteringAware
{
public ValueTask OnEnteringAsync(int personId)
{
var personContext = new PersonContext(personId);
navigationServiceProvider.AddNavigationScoped(personContext);
}
}
```Then you can inject the service in the nested page models through the `INavigationServiceProvider`:
```csharp
public class PersonDetailsPageModel(INavigationServiceProvider navigationServiceProvider) : IPersonDetailsPageModel
{
private readonly IPersonContext _personContext = navigationServiceProvider.GetRequiredService();
}
```#### How to unit test navigation
Here's an example of how to unit test navigation using `NSubstitute`:
Using `record` for intents is recommended to avoid having to implement an equality comparer.
Suppose to have defined an intent class `public record AnIntent(int Value = 0);`.```csharp
// Arrange
var navigationService = Substitute.For();
navigationService.GoToAsync(Arg.Any()).Returns(Task.FromResult(true));
var viewModel = new MyViewModel(navigationService);// Act
await viewModel.DoSomethingAsync(5);// Assert
var expectedNavigation = Navigation.Relative().Push().WithIntent(new AnIntent(5));
await navigationService.Received().GoToAsync(Arg.Is(n => n.Matches(expectedNavigation)));
```#### Sample application
This repository contains a `Nalu.Maui.Sample` project that shows how to use Nalu navigation.
Play with it to better see how it works.#### Do you just care about disposing page view model?
If you're here just because you want page/vm disposal on the standard `NavigationPage` or `Shell` and you don't want these awesome features, you can just use the following methods to enable calling `Dispose` on page models after page has been removed from navigation stack.
##### NavigationPage
```csharp
MainPage = new NavigationPage(new MainPage()).ConfigureForPageDisposal();
```##### Shell
```csharp
MainPage = new AppShell().ConfigureForPageDisposal();
```Note: shell content pages will not be disposed.