dotnet / dotnet/maui

[Enhancement] Give developers full control over `Shell` view creation by making all views use a `RouteFactory`

Open
#3,917 4 comments 8 reactions 0 assignees View on GitHub
area-controls-shell proposal/open
Dominant language
C#
Stars
23.3k
Forks
2k
Avg merge
1d 10h
Merged PRs (30d)
306

Description

### Description

I had hoped to submit this before you merged pull request #3375. The solution below would address the issues of there being a lack of dependency injection support for shell views as well as fixing the problem with view creation being fractured between `RouteFactory` and `ElementTemplate`, by making all shell views use a `RouteFactory`, as described below. The changes could be considered an alternate solution to the PR mentioned above.

The current way to use a custom `RouteFactory` is by registering routes using the `RegisterRoute(string, RouteFactory)` function and doing this gives you control over view creation. If desired a developer may use dependency injection when creating views, unfortunately this doesn't work for all routes. Any routes that are registered with `RegisterRoute(string, Type)` will use the default `TypeRouteFactory`, and any route on a shell page that uses a `DataTemplate` doesn't even use a `RouteFactory`. This is because when routes are specified as a `DataTemplate` on a shell page they are instantiated in `ElementTemplate`'s constructor using `Activator.CreateInstance(Type)` and never actually use a `RouteFactory`. Using a custom `RouteFactory` offers a decent way of controlling view creation during navigation, however if it isn't used for most routes, it loses most of its value. Using a custom `RouteFactory` for all shell views created should be something that a developer can opt into control should they want/need to. The changes required for making all shell views use a `RouteFactory` doesn't require anything major, in fact the default behavior should stay the same. All that is needed is to add a new private delegate to `Routing` that returns a `RouteFactory`, and an internal function that `ElementTemplate` can use to call the delegate thereby getting a `RouteFactory` to create the view. Then simply changing the `RegisterRoute(string, Type)` function to use the new delegate and change `ElementTemplate` to use the new function or delegate would be all that is needed to make all shell views use a `RouteFactory`. The complete details about the changes can be found in the API Changes section.

A branch called default-route-delegate with the suggested changes to the fork has been added to my account [here](https://github.com/Xero-9/maui/tree/default-route-delegate). The sample project has a new folder with an updated Xaminals sample and in the sample's `Startup` class I added Xaminals to the `PageType` enum to easily select which sample to use. The Xaminals sample also uses conditional compilation with two constants, `ENABLE_DI_CHANGES` and `USE_PARTIAL_DI`, the first one `ENABLE_DI_CHANGES` makes the suggested changes below to both `Routing` and `ElementTemplate`, then in the Xaminals sample it switches to using constructor injection for all its views. The second `USE_PARTIAL_DI` disables all the changes and instead shows how shell currently supports using DI with a custom `RouteFactory`. In `Directory.Build.props` there are 2 `` elements that are commented out for each of the constants to make switching for testing easier. If both constants are commented out, then the sample runs similar the original sample. Startup performance was measured by looking at how long it took and when the `AppShell` constructor started and finished, and on average they all ran around ~1.5s when compiled for Debug. Unfortunately, only performance for the Android emulator was measured, currently I don't have access to any MacOS or iOS devices, so I can't confirmation that this is the same for them.

### Public API Changes

The delegate should be a private or internal property with a private backing field that is set to use the default `TypeRouteFactory` when the backing field is null, ensuring that a valid `RouteFactory` is always used maintaining the current functionality. The delegate should have a route string parameter, while this is not mandatory for creating a view, however having the route is incredibly useful, and should be included when possible. The route parameter could also be used to indicate when the view is being created for an `ElementTemplate` by passing in a route that has a prefix or suffix that identifies where the call came from.
```CSharp
static Func s_defaultRouteFactory;
static Func DefaultRouteFactory => s_defaultRouteFactory ??= (str, type) => new TypeRouteFactory(type);
```
The `ElementTemplate` class should use an internal function to access the delegate, especially if a route is generated with a prefix or suffix, although if the delegate is made internal this could be done in `ElementTemplate`'s constructor. In the future this function could be used to change implicit routing to use a `RouteFactory` instead of storing the entire page object, however this is going beyond the scope of this enhancement. I do plan to submit another enhancement around improving implicit routing.
```CSharp
internal static object GetOrCreateContentFromTemplate(Type type)
{
return s_defaultRouteFactory(null, type).GetOrCreate();
}
```

In `ElementTemplate`'s constructor `LoadTemplate` needs to be set to use the new `GetOrCreateContentFromTemplate` function created above, or the delegate could be made internal and used directly but, I don't think this is the best way.
```CSharp
LoadTemplate = () => Routing.GetOrCreateContentFromTemplate(type);
```
Or
```CSharp
LoadTemplate = () => Routing.DefaultRouteFactory(null, type);
```
In `Routing` the `RegisterRoute(string, Type)` function needs to be updated to use the new delegate.
```CSharp
public static void RegisterRoute(string route, Type type)
{
RegisterRoute(route, DefaultRouteFactory(type, route));
}
```
Since the delegate should only be set at startup, some public extension methods for `MauiAppBuilder` to override the default `RouteFactory` accomplishes this perfectly. The extension methods should ensure that trying to set the delegate after it has already been used once or trying to set it more than once should result in an error.
```CSharp
public static MauiAppBuilder OverrideDefaultRouteFactory(this MauiAppBuilder builder, Func defaultRouteFactory)
{
if (defaultRouteFactory == null)
return builder;
if (s_defaultRouteFactory != null)
throw new InvalidOperationException("The default method for creating a Route Factory can only be set once before being called for the first time.");
s_defaultRouteFactory = defaultRouteFactory;
return builder;
}
public static MauiAppBuilder OverrideDefaultRouteFactory(this MauiAppBuilder builder) where TRouteFactory : RouteFactory
{
builder.OverrideDefaultRouteFactory((s, t) => Activator.CreateInstance(typeof(TRouteFactory), s, t) as RouteFactory);
return builder;
}
```

### Intended Use-Case

The general intended use for a custom `RouteFactory` is to have all views use constructor injection to resolve dependencies. If all shell views use a `RouteFactory` then the developer can opt in to have complete control over how the view is created including if and how it is cached. This also would be an ideal place to put view model resolution if you don't want the view model resolved with constructor injection.

Personally I would like to use attributes to control a source generator to handle registering all the services and routes. I have already built the source generator, but currently it will only work for views registered with the `RegisterRoute(string, RouteFactory)` function however most routes are not registered that way. With these changes anyone would be able to take full advantage of using a source generator for route and service registration.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.