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

Added Shell Page dependency resolution with DataTemplate and Shell global routes… #3375

Merged
merged 6 commits into from
Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 8 additions & 1 deletion src/Controls/src/Core/ElementTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ internal ElementTemplate(Type type) : this()

_canRecycle = true;

LoadTemplate = () => Activator.CreateInstance(type);
LoadTemplate = () =>
{
if (_parent?.FindMauiContext()?.Services != null)
{
return Extensions.DependencyInjection.ActivatorUtilities.GetServiceOrCreateInstance(_parent.FindMauiContext().Services, type);
}
return Activator.CreateInstance(type);
};
}

internal ElementTemplate(Func<object> loadTemplate) : this() => LoadTemplate = loadTemplate ?? throw new ArgumentNullException("loadTemplate");
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/RouteFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Microsoft.Maui.Controls
{
public abstract class RouteFactory
{
public abstract Element GetOrCreate();
public abstract Element GetOrCreate(IServiceProvider services);
brunck marked this conversation as resolved.
Show resolved Hide resolved
}
}
24 changes: 19 additions & 5 deletions src/Controls/src/Core/Routing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ internal static string[] GetRouteKeys()
return keys;
}

public static Element GetOrCreateContent(string route)
public static Element GetOrCreateContent(string route, IServiceProvider services)
PureWeen marked this conversation as resolved.
Show resolved Hide resolved
{
Element result = null;

Expand All @@ -136,14 +136,23 @@ public static Element GetOrCreateContent(string route)
}

if (s_routes.TryGetValue(route, out var content))
result = content.GetOrCreate();
result = content.GetOrCreate(services);

if (result == null)
{
// okay maybe its a type, we'll try that just to be nice to the user
var type = Type.GetType(route);
if (type != null)
result = Activator.CreateInstance(type) as Element;
{
if (services != null)
{
result = Extensions.DependencyInjection.ActivatorUtilities.GetServiceOrCreateInstance(services, type) as Element;
}
else
{
result = Activator.CreateInstance(type) as Element;
}
}
}

if (result != null)
Expand Down Expand Up @@ -232,10 +241,15 @@ public TypeRouteFactory(Type type)
_type = type;
}

public override Element GetOrCreate()
public override Element GetOrCreate(IServiceProvider services)
{
return (Element)Activator.CreateInstance(_type);
if (services != null)
{
return Extensions.DependencyInjection.ActivatorUtilities.GetServiceOrCreateInstance(services, _type) as Element;
}
return Activator.CreateInstance(_type) as Element;
}

public override bool Equals(object obj)
{
if ((obj is TypeRouteFactory typeRouteFactory))
Expand Down
8 changes: 4 additions & 4 deletions src/Controls/src/Core/Shell/ShellNavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public async Task GoToAsync(ShellNavigationParameters shellNavigationParameters)
modalStackPreBuilt = true;

bool? isAnimated = (nextActiveSection != currentShellSection) ? false : animate;
await nextActiveSection.GoToAsync(navigationRequest, parameters, isAnimated, isRelativePopping);
await nextActiveSection.GoToAsync(navigationRequest, parameters, _shell.FindMauiContext()?.Services, isAnimated, isRelativePopping);
}

if (shellItem != null)
Expand Down Expand Up @@ -149,7 +149,7 @@ public async Task GoToAsync(ShellNavigationParameters shellNavigationParameters)
// TODO get rid of this hack and fix so if there's a stack the current page doesn't display
await Device.InvokeOnMainThreadAsync(() =>
{
return _shell.CurrentItem.CurrentItem.GoToAsync(navigationRequest, parameters, animate, isRelativePopping);
return _shell.CurrentItem.CurrentItem.GoToAsync(navigationRequest, parameters, _shell.FindMauiContext()?.Services, animate, isRelativePopping);
});
}
else if (navigationRequest.Request.GlobalRoutes.Count == 0 &&
Expand All @@ -159,13 +159,13 @@ await Device.InvokeOnMainThreadAsync(() =>
// TODO get rid of this hack and fix so if there's a stack the current page doesn't display
await Device.InvokeOnMainThreadAsync(() =>
{
return _shell.CurrentItem.CurrentItem.GoToAsync(navigationRequest, parameters, animate, isRelativePopping);
return _shell.CurrentItem.CurrentItem.GoToAsync(navigationRequest, parameters, _shell.FindMauiContext()?.Services, animate, isRelativePopping);
});
}
}
else
{
await _shell.CurrentItem.CurrentItem.GoToAsync(navigationRequest, parameters, animate, isRelativePopping);
await _shell.CurrentItem.CurrentItem.GoToAsync(navigationRequest, parameters, _shell.FindMauiContext()?.Services, animate, isRelativePopping);
}

(_shell as IShellController).UpdateCurrentState(source);
Expand Down
14 changes: 7 additions & 7 deletions src/Controls/src/Core/Shell/ShellSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ public static implicit operator ShellSection(TemplatedPage page)
return (ShellSection)(ShellContent)page;
}

async Task PrepareCurrentStackForBeingReplaced(ShellNavigationRequest request, ShellRouteParameters queryData, bool? animate, List<string> globalRoutes, bool isRelativePopping)
async Task PrepareCurrentStackForBeingReplaced(ShellNavigationRequest request, ShellRouteParameters queryData, IServiceProvider services, bool? animate, List<string> globalRoutes, bool isRelativePopping)
{
string route = "";
List<Page> navStack = null;
Expand Down Expand Up @@ -350,7 +350,7 @@ async Task PrepareCurrentStackForBeingReplaced(ShellNavigationRequest request, S
continue;
}

var page = GetOrCreateFromRoute(globalRoutes[i], queryData, i == globalRoutes.Count - 1, false);
var page = GetOrCreateFromRoute(globalRoutes[i], queryData, services, i == globalRoutes.Count - 1, false);
if (IsModal(page))
{
await PushModalAsync(page, IsNavigationAnimated(page));
Expand Down Expand Up @@ -468,9 +468,9 @@ void RemoveExcessPathsWithinTheRoute()
}
}

Page GetOrCreateFromRoute(string route, ShellRouteParameters queryData, bool isLast, bool isPopping)
Page GetOrCreateFromRoute(string route, ShellRouteParameters queryData, IServiceProvider services, bool isLast, bool isPopping)
{
var content = Routing.GetOrCreateContent(route) as Page;
var content = Routing.GetOrCreateContent(route, services) as Page;
if (content == null)
{
Internals.Log.Warning(nameof(Shell), $"Failed to Create Content For: {route}");
Expand All @@ -480,7 +480,7 @@ Page GetOrCreateFromRoute(string route, ShellRouteParameters queryData, bool isL
return content;
}

internal async Task GoToAsync(ShellNavigationRequest request, ShellRouteParameters queryData, bool? animate, bool isRelativePopping)
internal async Task GoToAsync(ShellNavigationRequest request, ShellRouteParameters queryData, IServiceProvider services, bool? animate, bool isRelativePopping)
{
List<string> globalRoutes = request.Request.GlobalRoutes;
if (globalRoutes == null || globalRoutes.Count == 0)
Expand All @@ -493,7 +493,7 @@ internal async Task GoToAsync(ShellNavigationRequest request, ShellRouteParamete
return;
}

await PrepareCurrentStackForBeingReplaced(request, queryData, animate, globalRoutes, isRelativePopping);
await PrepareCurrentStackForBeingReplaced(request, queryData, services, animate, globalRoutes, isRelativePopping);

List<Page> modalPageStacks = new List<Page>();
List<Page> nonModalPageStacks = new List<Page>();
Expand All @@ -511,7 +511,7 @@ internal async Task GoToAsync(ShellNavigationRequest request, ShellRouteParamete
for (int i = whereToStartNavigation; i < globalRoutes.Count; i++)
{
bool isLast = i == globalRoutes.Count - 1;
var content = GetOrCreateFromRoute(globalRoutes[i], queryData, isLast, false);
var content = GetOrCreateFromRoute(globalRoutes[i], queryData, services, isLast, false);
if (content == null)
{
break;
Expand Down
41 changes: 41 additions & 0 deletions src/Controls/tests/Core.UnitTests/DataTemplateTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Microsoft.Maui.Controls;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Microsoft.Maui;

namespace Microsoft.Maui.Controls.Core.UnitTests
{
Expand Down Expand Up @@ -118,5 +122,42 @@ public void SetValueAndBinding()
};
Assert.That(() => template.CreateContent(), Throws.InstanceOf<InvalidOperationException>());
}

[Test]
public void CreateContentWithDependencyResolution()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<Dependency>();
IServiceProvider services = serviceCollection.BuildServiceProvider();
var fakeMauiContext = Substitute.For<IMauiContext>();
var fakeHandler = Substitute.For<IElementHandler>();
fakeMauiContext.Services.Returns(services);
fakeHandler.MauiContext.Returns(fakeMauiContext);
var fakeApplication = new Application();
fakeApplication.Handler = fakeHandler;
Application.Current = fakeApplication;

var template = new DataTemplate(typeof(PageWithDependency));
var obj = template.CreateContent();
Assert.That(obj, Is.InstanceOf<PageWithDependency>());
var page = obj as PageWithDependency;
Assert.That(page, Is.Not.Null);
Assert.That(page.TestDependency, Is.Not.Null);
}

class PageWithDependency : ContentPage
{
public Dependency TestDependency { get; set; }

public PageWithDependency(Dependency dependency)
{
TestDependency = dependency;
}
}

class Dependency
{
public int Test { get; set; }
}
}
}
49 changes: 49 additions & 0 deletions src/Controls/tests/Core.UnitTests/ShellNavigatingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls.Internals;
using NUnit.Framework;
using NSubstitute;
using Microsoft.Maui;

namespace Microsoft.Maui.Controls.Core.UnitTests
{
Expand Down Expand Up @@ -620,6 +623,37 @@ public async Task RouteWithGlobalPageRoute()
Assert.AreEqual("//animals/domestic/cats/catdetails", shell.CurrentState.Location.ToString());
}

[TestCase(typeof(PageWithDependency), typeof(PageWithDependency))]
[TestCase(typeof(PageWithDependency), typeof(Dependency))]
public async Task GlobalRouteWithDependencyResolution(Type typeForRouteName, Type type)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<Dependency>();
IServiceProvider services = serviceCollection.BuildServiceProvider();
var fakeMauiContext = Substitute.For<IMauiContext>();
var fakeHandler = Substitute.For<IElementHandler>();
fakeMauiContext.Services.Returns(services);
fakeHandler.MauiContext.Returns(fakeMauiContext);

var flyoutItem = CreateShellItem<FlyoutItem>();
flyoutItem.Items.Add(CreateShellContent(asImplicit: true, shellContentRoute: "cats"));
var shell = new TestShell
{
Items = { flyoutItem }
};
shell.Parent.Handler = fakeHandler;
var routeName = typeForRouteName.AssemblyQualifiedName;
Routing.RegisterRoute(routeName, type);
await shell.GoToAsync(routeName);

Assert.IsNotNull(shell.Navigation);
Assert.IsNotNull(shell.Navigation.NavigationStack);
var page = shell.Navigation.NavigationStack[1];
Assert.That(page, Is.Not.Null);
Assert.IsInstanceOf<PageWithDependency>(page);
Assert.That((page as PageWithDependency).TestDependency, Is.Not.Null);
}

[Test]
public async Task AbsoluteRoutingToPage()
{
Expand Down Expand Up @@ -929,5 +963,20 @@ protected override Task OnPushModal(Page modal, bool animated)

ShellNavigatingEventArgs CreateShellNavigatedEventArgs() =>
new ShellNavigatingEventArgs("..", "../newstate", ShellNavigationSource.Push, true);

public class PageWithDependency : ContentPage
{
public Dependency TestDependency { get; set; }

public PageWithDependency(Dependency dependency)
{
TestDependency = dependency;
}
}

public class Dependency
{
public int Test { get; set; }
}
}
}
2 changes: 1 addition & 1 deletion src/Controls/tests/Core.UnitTests/ShellTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ public ConcretePageFactory(ContentPage contentPage)
_contentPage = contentPage;
}

public override Element GetOrCreate() => _contentPage;
public override Element GetOrCreate(IServiceProvider services) => _contentPage;
}

public Action<ShellNavigatedEventArgs> OnNavigatedHandler { get; set; }
Expand Down
4 changes: 3 additions & 1 deletion src/Controls/tests/Core.UnitTests/ShellTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using NSubstitute;
using NUnit.Framework;

namespace Microsoft.Maui.Controls.Core.UnitTests
Expand Down Expand Up @@ -259,13 +260,14 @@ public void SimpleGoTo()
public async Task CaseIgnoreRouting()
{
var routes = new[] { "Tab1", "TAB2", "@-_-@", "+:~", "=%", "Super_Simple+-Route.doc", "1/2", @"1\2/3", "app://tab" };
var services = Substitute.For<IServiceProvider>();

foreach (var route in routes)
{
var formattedRoute = Routing.FormatRoute(route);
Routing.RegisterRoute(formattedRoute, typeof(ShellItem));

var content1 = Routing.GetOrCreateContent(formattedRoute);
var content1 = Routing.GetOrCreateContent(formattedRoute, services);
Assert.IsNotNull(content1);
Assert.AreEqual(Routing.GetRoute(content1), formattedRoute);
}
Expand Down