-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Support deep linking into .NET MAUI Blazor apps #3788
Comments
@Eilon I know Shell navigation was added to .NET MAui Blazor Apps Does that work provide deep linking into .NET MAUI Blazor Apps if you are using Shell? PR for reference |
@PureWeen we might need to build something similar, but not likely quite the same. I think that feature in Mobile Blazor Bindings (MBB) was for native UI built using Blazor (in MBB we supported true native UI using Xamarin.Forms w/ Blazor syntax, and also using web UI in a BlazorWebView). I don't recall us doing anything for deep-linking into web UI, but it might contain some similarities. |
@Eilon makes sense! The routing parts of Shell are all very custom built so also seeing if there's anything here we can reuse/leverage as well |
We've moved this issue to the Future milestone. This means that it is not going to be worked on for the coming release. We will reassess the issue following the current release and consider this item at that time. |
Hi, excuse me if this is not the right place to ask. |
@Ghevi I've never played with deep linking, but in general the strategy would be something like this:
Unfortunately I don't know anything about part (1), but I would say it's highly likely that it's either exactly the same or almost exactly the same as how to do it in Xamarin.Forms apps, so perhaps there are good docs for Xamarin.Forms that could shed some light on it? |
@Eilon I managed to do part (1), it was nice to learn how to make it work btw. Also thank You for the help! |
@Ghevi this is well outside my expertise, but I think for (2) it depends on what your app's UI looks like. For example, if your app is based on the .NET MAUI Shell control, then I think you would tell the Shell control to navigate to the relevant part of the URL, and you can read more about that here: https://docs.microsoft.com/dotnet/maui/fundamentals/shell/navigation. And if that page happens to contain a BlazorWebView, you could wait until it's done loading and send it any additional navigation information. For example, let's say the URL is |
@Eilon I've read about the Shell and how to use it. But in working on a MAUI Blazor project, so there is no Shell unless I'm not aware of a way to use it in such case. Will do some more research, thank you for the help! |
@Ghevi the Shell was just an example, so it depends on how your app is designed. The point in (2) is that if the deep link goes to a page in your app that is built with BlazorWebView, but that page isn't visible, you'll need to first make that page visible, and then do whatever Blazor navigation is needed. |
@Eilon yes, thanks i've understood what you ment. I improved my solution by using a service instead of the |
Ok after a bit more debugging I got it working in a similar way, but from a separate custom Activity instead of in MainActivity. I have a custom Activity which is invoked when a particular URL is opened with the app. e.g https://myapp/go-here I created a static From the Activity I just set That looks like this: protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
var data = intent.DataString;
if (intent.Action != Intent.ActionView) return;
if (string.IsNullOrWhiteSpace(data)) return;
var path = data.Replace(@"https://myapp", "");
NavigationService.SetPage(path);
} In That looks like this: @code
{
[Inject]
public NavigationManager NavigationManager { get; set; }
private static bool _subscribedToNavigationPageSetEvent;
protected override void OnInitialized()
{
if (!_subscribedToNavigationPageSetEvent)
{
NavigationService.PageSet += NavigationServiceOnPageSet;
_subscribedToNavigationPageSetEvent = true;
}
}
private void NavigationServiceOnPageSet(object sender, object e)
{
if (!string.IsNullOrWhiteSpace(NavigationService.Page))
{
Debug.WriteLine(NavigationService.Page);
NavigationManager.NavigateTo(NavigationService.Page, forceLoad:true);
NavigationService.SetPage("");
}
}
} And for convenience, here's my NavigationService code: public static class NavigationService
{
public delegate void PageSetEventHandler(object sender, object e);
public static event PageSetEventHandler PageSet;
public static string Page { get; private set; }
public static void SetPage(string url)
{
Page = url;
if (!string.IsNullOrEmpty(url))
{
PageSet?.Invoke(null, url);
}
}
} Not ideal, but it works! |
@MikeAndrews90 could you share your NavigationService code please. Also, how to do deep linking for ios? |
@GolfJimB It doesn't do much, just sets the current page path public interface INavService
{
public string Page { get; }
void SetPage(string url);
}
public static class NavigationService
{
public static string Page { get; private set; }
public static void SetPage(string url)
{
Page = url;
}
} I'm not targeting iOS so I don't know how to do deep linking on iOS. |
For iOS what worked for me was adding this method to the
then add in the |
@Ghevi what's the |
@GolfJimB two classes i've made. The service contains the dto and has some other methods. |
For anyone coming to this later, my comment above with my solution still works, however I've just edited it with something important. Previously, I was running StartActivity(typeof(MainActivity)) - However I have just discovered today that if you keep doing that, the app gets extremely laggy, I suspect its actually running the whole app/Blazor on top of the existing one every time its invoked, which obviously will be using a load of resources. I found a workaround, and that's by using an event handler, rather than executing StartActivity. |
@MikeAndrews90 Can you share a code snippet of using an event handler instead of |
@tpmccrary I updated my comment above with it all in #3788 (comment) |
@MikeAndrews90 Thanks for updating it! One other question. I noticed when the app is opened, deep linking works perfect with your approach. However, if the app is fully closed, deep linking does not work. It just sends me to the initial app page. |
Don't think I have... Have you done an all the assetlinks.json stuff? |
@MikeAndrews90 I have not. What do I have to do to get that setup? Thanks for all the help by the way! |
You need an assetlinks.json file at the domain you're using for deep linking, e.g https://myapp.com/.well-known/assetlinks.json A quick Google found an example in this answer on stack overflow: To get your sha256 fingerprint for the json file follow Microsoft's docs https://learn.microsoft.com/en-us/xamarin/android/deploy-test/signing/keystore-signature?tabs=windows |
@MikeAndrews90 Would you be willing to share the contents of the |
@tpmccrary Here's my UrlActivity for the deeplinking: (The casing is a bit weird because I was in a rush to get it working, and havne't got round to sorting it, and it has to match in the manifest XML) namespace myapp.app
{
[IntentFilter(new[] { Intent.ActionView },
Categories = new[]
{
Intent.CategoryBrowsable
},
DataScheme = "https",
AutoVerify = true,
DataHost = "myapp.com",
DataMimeType = "com.yourcompany.yourapp"
)
]
[Activity(Name = "myapp.app.urlactivity",
MainLauncher = false,
Theme = "@style/Maui.SplashTheme",
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class urlactivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
SetTheme(Resource.Style.AppTheme);
base.OnCreate(savedInstanceState);
OnNewIntent(Intent);
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
var data = intent.DataString;
if (intent.Action != Intent.ActionView) return;
if (string.IsNullOrWhiteSpace(data)) return;
var path = data.Replace(@"https://myapp.com", "");
NavigationService.SetPage(path);
}
} And namespace MyApp.App
{
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
[IntentFilter(new[] { NfcAdapter.ActionNdefDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/com.yourcompany.yourapp")]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
protected override void OnResume()
{
base.OnResume();
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
}
// app minimise
protected override void OnPause()
{
ConnectivityStatus.Stop();
base.OnPause();
}
// app close
protected override void OnDestroy()
{
base.OnDestroy();
}
}
} And don't forget to add it into your AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<application android:allowBackup="true" tools:replace="android:allowBackup,android:label" android:icon="@mipmap/icon_background" android:roundIcon="@mipmap/icon_background" android:supportsRtl="true">
<activity android:name="myapp.app.urlactivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="myapp.com" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest> |
Could you please share the project, to see the complete code? |
All of this just does not seem to work for me. It would be really very helpful to see a project, that works end-to-end with the above approaches. |
Just to share my experiences, for Android specifically (still need to get around to iOS), few things that helped me, in addition to all the code shared from above:
// Will both automatically be written to the generated `AndroidManifest.xml` file on build (check `..\obj\Debug\net7.0-android\AndroidManifest.xml`)
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
AutoVerify = true, DataScheme = "https", DataHost = "mysite.com")]
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density,
// Prevents MainActivitiy from being re-created on launching an intent (also makes it to where `OnNewIntent` will be called directly, if the app has already been loaded)
LaunchMode = LaunchMode.SingleTop
)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// In case the app was opened (on first load) with an `ActionView` intent
OnNewIntent(this.Intent);
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
var data = intent.DataString;
if (intent.Action != Intent.ActionView) return;
if (string.IsNullOrWhiteSpace(data)) return;
// TODO: Call some sort of propogation service to pass down `data`, for example:
var appServices = ServiceHelper.Current.GetRequiredService<AppServices>(); // MAUI cross-platform service resolver: https://stackoverflow.com/a/73521158/10388359
appServices.OnAppLinkReceived(data);
}
}
public class AppLinkReceivedEventArgs : EventArgs
{
public required string Data { get; set; }
}
// Don't forget to register this in DI as a singleton
public class AppServices
{
public event EventHandler<AppLinkReceivedEventArgs>? AppLinkReceived;
public string? LastAppLink { get; private set; }
public void OnAppLinkReceived(string data)
{
LastAppLink = data;
AppLinkReceived?.Invoke(this, new() { Data = data });
}
}
Some other useful ADB commands for debugging android deep links (see also): # Resets verified app link states for your app
adb shell pm set-app-links --package com.companyname.myapp 0 all
# Manually trigger verification for your app (will be auto triggered with `android:autoVerify="true"`)
adb shell pm verify-app-links --re-verify com.companyname.myapp
# Manually trigger an `action.VIEW` intent
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://mysite.com/some/path?q=1"
@implements IDisposable
@inject AppServices appServices
@code {
protected override void OnInitialized()
{
appServices.AppLinkReceived += AppServices_AppLinkReceived;
if (!string.IsNullOrEmpty(appServices.LastAppLink))
{
AppServices_AppLinkReceived(null, new() { Data = appServices.LastAppLink });
}
}
private void AppServices_AppLinkReceived(object? sender, AppLinkReceivedEventArgs e)
{
// TODO: Parse `e.Data` and determine how to route it
}
void IDisposable.Dispose()
{
appServices.AppLinkReceived -= AppServices_AppLinkReceived;
}
} Note that in my use case I didn't need to actually Navigate to any page, so I'm not currently using the If anyone ever gets to a point of putting together a full end-to-end example, just an idea, but I think it might be possible to use Github pages to host the |
@gerneio , thanks for your sample. |
@SamVanhoutte I would put this in your top most common layer that's shared across all your pages. So maybe
@inherits LayoutComponentBase
@implements IDisposable
@inject AppServices appServices
@inject NavigationManager nav
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@code {
protected override void OnInitialized()
{
appServices.AppLinkReceived += AppServices_AppLinkReceived;
if (!string.IsNullOrEmpty(appServices.LastAppLink))
{
AppServices_AppLinkReceived(null, new() { Data = appServices.LastAppLink });
}
}
private void AppServices_AppLinkReceived(object? sender, AppLinkReceivedEventArgs e)
{
// Assuming e.Data represents a URL value
// Examples: `https://myhost.com/counter?initCount=12345` or `myapp://mobile/counter?initCount=12345`
if (Uri.TryCreate(e.Data, UriKind.Absolute, out var uri))
{
var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
// TODO: Do some additional checking to make sure URL is in expected format
if (path.Equals("counter", StringComparison.OrdinalIgnoreCase))
{
var qp = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
// TODO: Do some checks to make sure param is of expected type/format?
if (qp.TryGetValue("initCount", out var cnt))
{
// Navigate to `counter` page and pass in `initCount` param
nav.NavigateTo($"counter?initCount={cnt}");
}
}
}
}
void IDisposable.Dispose()
{
appServices.AppLinkReceived -= AppServices_AppLinkReceived;
}
}
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>
@functions {
[Parameter]
[SupplyParameterFromQuery(Name = "initCount")]
public int currentCount { get; set; } = 0;
void IncrementCount()
{
currentCount++;
}
} Note that the above code is completely untested, but I suspect it will generally work, maybe with a few tweaks. You can look at the docs to learn more about Blazor navigation. Also, while you could probably just parse out the url path and query string and have the built-in Blazor router handle it through the Hope this helps! |
Thank you so much! Finally, I got this working! Right in time. Very much appreciated. |
@gerneio Did you face any issue when testing app link when the app is closed. I am unable to get that to work. I have a similar setup with the exception:
I get an exception
This is only when I try to open my app when its closed from the applink. When I have the app open and in background it works as expected and no crash. Mostly because of some timing issue? Not too sure. I did try something similar to yours, i.e. using a custom event handler, still the same error. Update: For anyone facing a similar issue, I solved the issue by moving to .net 8 preview. #9658 had a similar crash log and a fix assigned ( which is not back ported to .net 7), I thought I'll give it a try and that worked. |
Hi everyone, our docs writers have published a doc on Deep Linking that you can read here: https://learn.microsoft.com/aspnet/core/blazor/hybrid/routing?view=aspnetcore-8.0&pivots=maui#deep-linking |
Description
Investigate supporting deep linking into pages in a .NET MAUI Blazor app
Public API Changes
TBD
Intended Use-Case
Use a link to navigate to a page in a .NET MAUI Blazor app
The text was updated successfully, but these errors were encountered: