maui icon indicating copy to clipboard operation
maui copied to clipboard

Support deep linking into .NET MAUI Blazor apps

Open danroth27 opened this issue 3 years ago β€’ 15 comments
trafficstars

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

danroth27 avatar Dec 16 '21 15:12 danroth27

@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 https://github.com/dotnet/MobileBlazorBindings/pull/152

PureWeen avatar Dec 16 '21 18:12 PureWeen

@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 avatar Dec 16 '21 22:12 Eilon

@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 But the overlap might be very minimal

PureWeen avatar Dec 17 '21 18:12 PureWeen

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.

ghost avatar May 04 '22 17:05 ghost

Hi, excuse me if this is not the right place to ask. Since deep linking is not yet supported out of the box in Maui, I tried implementing it in the native code, for example with android, in the MainActivity.cs file. It works and I managed to pass parameters and access them with intent.dataString. In order to access this data in the razor components, I save it in MainActivity.cs with the Preferences api and then get it where I needed. This works but it's a bit cumbersome. The messaging center can't be use yet because razor components cannot registers callbacks for these messages at this stage, right? I tried this protected override void OnAppLinkRequestReceived(Uri uri) in App.xaml.cs file but it doesn't seems to be called unless I made some mistake. Any other solution? If you want me I will move this to the discussions section.

Ghevi avatar Jul 05 '22 09:07 Ghevi

@Ghevi I've never played with deep linking, but in general the strategy would be something like this:

  1. Hook into however each platform does its deep linking and get the requested link
  2. If the link goes to content that is in a BlazorWebView, then do the necessary MAUI actions to make sure that BlazorWebView is visible and active
  3. Once the BlazorWebView is ready and loaded, use Blazor's navigation APIs to go to the exact content that was requested

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 avatar Jul 05 '22 16:07 Eilon

@Eilon I managed to do part (1), it was nice to learn how to make it work btw. Unfortunately I'm a bit lost on part (2). In Xamarin I found that there was a method called LoadApplication(myString) that you can call in MainActivity.cs hooks, but there isn't in Maui right? Basically I'm not sure how to pass a piece of data from platform specific code, in this case MainActivity.cs to for example a Razor Component with the @page tag or even to the App.xaml.cs. I mean I managed to do it with the Preferences api but again I feel like it's not the best solution.

Also thank You for the help!

Ghevi avatar Jul 06 '22 08:07 Ghevi

@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 /myApp/users/info/12345. The myApp/users page is a Shell page, so you would take that part of the URL and tell Shell to navigate there. But what about the info/12345? Let's say that page within Shell uses a BlazorWebView, so once that view is done loading, you would pass that info/12345 to the Blazor navigation system to make sure the right user info page is shown.

Eilon avatar Jul 06 '22 16:07 Eilon

@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 avatar Jul 07 '22 12:07 Ghevi

@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 avatar Jul 11 '22 18:07 Eilon

@Eilon yes, thanks i've understood what you ment. I improved my solution by using a service instead of the Preferences api, so i have more control over it. Before navigating to the index component, i check if there is any deep link data in my service and in that case use _navigationManager.NavigateTo("/deepLinkPage");. At this point the BlazorWebView is visible so it works. Thank you for the suggestions :)

Ghevi avatar Jul 14 '22 15:07 Ghevi

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 NavigationService type class, where I can set a string which is the path taken from the incoming URL from the activity. In NavigationService I also added an event handler that I fire when the page is set, so it can be subscribed to somewhere in Blazor (I did mine in Main.razor) and then you can use the NavigationManager to navigate to the page.

From the Activity I just set NavigationService.Path = "go-here";

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 Main.razor I added a code block which just checks the NavigationService to see if there's a path set, and if so, it uses the standard NavigationManager to navigate to the path just set from the Activity.

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 avatar Oct 31 '22 18:10 MikeAndrews90

@MikeAndrews90 could you share your NavigationService code please. Also, how to do deep linking for ios?

GolfJimB avatar Nov 12 '22 04:11 GolfJimB

@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.

MikeAndrews90 avatar Nov 12 '22 08:11 MikeAndrews90

For iOS what worked for me was adding this method to the AppDelegate.cs class:

[Export("application:openURL:options:")]
   public override Boolean OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
   {
       var deepLinkService = DependencyService.Get<DeepLinkService>();
       deepLinkService.DeepLinkDto = null;
       if (!String.IsNullOrEmpty(url.AbsoluteString) && url.AbsoluteString.Contains("myapp"))
       {
           if (url.AbsoluteString.Contains("/resources-page"))
           {
               var resourceId= NSUrlComponents
                   .FromString(url.Query)?.QueryItems?
                   .Single(x => x.Name == "resourceId").Value;
               deepLinkService.DeepLinkDto = new DeepLinkDto(resourceId);
           }
       }

       return true;
 }

then add in the Entitlements.plist the options Associated Domains with a string applinks:www.myappurl.com and also in the Identifiers section of https://developer.apple.com/account/resources/identifiers/list the AssociatedDomains to the capabilities. Keep in mind that if you want the app to open immediately without user prompt you have to go host an assetlinks.json file in one of your websites for android

Ghevi avatar Nov 15 '22 14:11 Ghevi

@Ghevi what's the DeepLinkService and DeepLinkDto?

GolfJimB avatar Dec 13 '22 07:12 GolfJimB

@GolfJimB two classes i've made. The service contains the dto and has some other methods.

Ghevi avatar Dec 13 '22 14:12 Ghevi

I started an investigation on deep linking with .NET MAUI + Blazor Hybrid and I'm fairly sure that there are enough features to implement it in an app.

Here's what I've found so far:

  1. You should be able to follow various Xamarin-based tutorials such as: https://www.xamarinhelp.com/android-app-links/
    • The same tutorials should apply almost exactly in .NET MAUI as well, because it's based on the same fundamental system
  2. Depending on the deep linking you want to do, you need to have a file on your web site in the .well-known/assetlinks.json location that has some metadata in it indicating which URLs can deep link into your app

To handle deep linking in your app:

  1. Use that platform's system for accepting deep links.
    • This likely involves changing your app's web site to indicate which apps can accept the links
    • This likely involves changing your app's manifest to indicate the accepted links
    • This likely involves overriding/implementing a .NET API in your app's platform-specific code to detect that it was launched via a deep link (for example, in Android you'd change Platforms\Android\MainActivity.cs and override OnCreate)
  2. In a case where your app was launched via a deep link, store the relevant parts of the deep link into a static field, such as public static string _blazorDeepLinkStartPath;
  3. In the part of your app that uses a BlazorWebView, check if a deep link was used, and if so, set it:
     if (_blazorDeepLinkStartPath != null)
     {
     	blazorWebView.StartPath = _blazorDeepLinkStartPath; // This is a new feature added in .NET MAUI 8
     	_blazorDeepLinkStartPath = null;
     }
    

I feel that for right now I've spent enough time researching this that I'm confident it can work, but testing it end-to-end involves a lot more work that I'm unable to perform right now. Plus, it seems like at least some people have demonstrated doing this successfully, so I'm even more confident it's possible.

I think the eventual goal here is to have better and more complete documentation on how to put together the different pieces, as opposed to adding any features to .NET MAUI / Blazor Hybrid itself.

Eilon avatar Feb 07 '23 22:02 Eilon

@Eilon , I am trying something like that, but I cannot find out which event handler I should override to navigation to the right location (using that static variable that I have saved earlier). Could you elaborate on that?

What I am currently doing (in order to open a scheme-based link), is the following (for iOS) logic in the AppDelegate class:

public override bool OpenUrl(UIApplication application, NSUrl url, NSDictionary options)
{
    AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url);
    if (url?.Scheme?.Equals(myScheme, StringComparison.InvariantCultureIgnoreCase) ?? false)
    {
        var pageUrl = url.ToString().Replace($"{myScheme}://", "");
        PageSettings.RequestedUri = pageUrl; // This is the static class/var I want to leverage in BlazorWebView
        return base.OpenUrl(application, new NSUrl( pageUrl), options);
    }
    return base.OpenUrl(application, url, options);
}

SamVanhoutte avatar Feb 08 '23 09:02 SamVanhoutte

I faced the same issue on iOS. I mean that I can open my app using a Universal link, but the method "OpenUrl" in AppDelegate.cs isn't triggering. Also, the method OnAppLinkRequestReceived in App.xaml.cs isn't triggering as well.

AndriyGlodan avatar Feb 09 '23 17:02 AndriyGlodan

+1 definitely would like to see some official documentation on how to enable deep linking for each platform. While there is plenty of documentation on the Android/iOS specific setups (manifest, etc), outside of this thread, there is very little code to show how to handle the deep linking once in the MAUI app. Additional docs on how to transfer deep linking down into the BlazorWebView would be appreciated too. In general, it looks like we have all the pieces to get this working for each platform, just scattered in different threads and implementations.

gerneio avatar Feb 14 '23 17:02 gerneio

Moving this to a later milestone in .NET 8 to cover what @Eilon has written above in documentation.

mkArtakMSFT avatar Feb 15 '23 17:02 mkArtakMSFT

@guardrex can you give this a try and see if you can document this and validate yourself, as you're documenting it?

mkArtakMSFT avatar Feb 15 '23 17:02 mkArtakMSFT

@mkArtakMSFT ... I've looked this over. To surface @Eilon's remarks temporarily, I cross-linked this issue into the Blazor Hybrid Routing and Navigation article. Unfortunately, I just learned that we haven't activated 8.0 preview content yet, so it can't be seen at this time. I versioned it for 8.0 per @Eilon's remark that blazorWebView.StartPath is an 8.0 feature.

WRT ...

... document this and validate yourself

Well ... πŸ€” ... maybe ... at great cost to the budget perhaps. I'm a web dev with limited desktop experience. If it would take @Eilon (emphasis added) "a lot more work that I'm unable to perform right now," I assume that it would take me considerably longer than that πŸ’°πŸ˜….

Do you want me to try? If so, what's the priority on this relative to the Blazor Security node work? I'll need perhaps a month to update all of the Blazor security docs. Can this wait until I get past the security work?

guardrex avatar Feb 16 '23 15:02 guardrex

I was able to resolve this issue on iOS and Android.

iOS workaround: Add these two methods in the AppDelegate.cs file:

public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity,
        UIApplicationRestorationHandler completionHandler)
    {
        CheckForAppLink(userActivity);
        return base.ContinueUserActivity(application, userActivity, completionHandler);
    }

    /// <summary>
    /// A method to check if an application has been opened using a Universal link.
    /// iOS implementation.
    /// </summary>
    /// <param name="userActivity"></param>
    private void CheckForAppLink(NSUserActivity userActivity)
    {
        var strLink = string.Empty;

        switch (userActivity.ActivityType)
        {
            case "NSUserActivityTypeBrowsingWeb":
                strLink = userActivity.WebPageUrl.AbsoluteString;
                break;
            case "com.apple.corespotlightitem":
                if (userActivity.UserInfo.ContainsKey(CSSearchableItem.ActivityIdentifier))
                    strLink = userActivity.UserInfo.ObjectForKey(CSSearchableItem.ActivityIdentifier).ToString();
                break;
            default:
                if (userActivity.UserInfo.ContainsKey(new NSString("link")))
                    strLink = userActivity.UserInfo[new NSString("link")].ToString();
                break;
        }

        if (!string.IsNullOrEmpty(strLink))
            App.Current.SendOnAppLinkRequestReceived(new Uri(strLink));
    }`

Android workaround: Add these two methods in the MainActivity.cs file

    protected override void OnNewIntent(Intent intent)
    {
        base.OnNewIntent(intent);
        CheckForAppLink(intent);
    }
    
    /// <summary>
    /// A method to check if an application has been opened using a Universal link.
    /// Android implementation.
    /// </summary>
    /// <param name="intent"></param>
    private void CheckForAppLink(Intent intent)
    {
        var action = intent.Action;
        var strLink = intent.DataString;
        if (Intent.ActionView != action || string.IsNullOrWhiteSpace(strLink))
            return;

        var link = new Uri(strLink);
        App.Current?.SendOnAppLinkRequestReceived(link);
    }

These implementations were taken from Xamarin.Forms source code. iOS: https://github.com/xamarin/Xamarin.Forms/blob/caab66bcf9614aca0c0805d560a34e176d196e17/Xamarin.Forms.Platform.iOS/FormsApplicationDelegate.cs#L155 Android: https://github.com/xamarin/Xamarin.Forms/blob/9df691e85d8c24486d71b9a502726f9835aad0f7/Xamarin.Forms.Platform.Android/AppCompat/FormsAppCompatActivity.cs#L508

AndriyGlodan avatar Feb 21 '23 17:02 AndriyGlodan

Thanks @guardrex. Leave the validation to us. that's ok.

mkArtakMSFT avatar Feb 22 '23 22:02 mkArtakMSFT

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 avatar Mar 20 '23 15:03 MikeAndrews90

@MikeAndrews90 Can you share a code snippet of using an event handler instead of StartActivity? Thanks in advance!

tpmccrary avatar Mar 27 '23 19:03 tpmccrary

@tpmccrary I updated my comment above with it all in https://github.com/dotnet/maui/issues/3788#issuecomment-1297529268

MikeAndrews90 avatar Mar 27 '23 20:03 MikeAndrews90

@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. Have you experienced this?

tpmccrary avatar Mar 27 '23 20:03 tpmccrary