Forum Discussion

Steve_PP's avatar
Steve_PP
Experienced User
1 year ago

C# WebBrowser -> WebView2 solution

Hi

 

Here is a solution for those with C#.NET apps that need to update the internal browser their desktop app invokes when OAuth is required due to breaking changes enforced by MYOB in Aug 2024. In summary, you need to replace WebBrowser with WebView2 (NuGet: Microsoft.Web.WebView2)

 

If you used the MYOB template of many years ago, you can replace OAuthLogin.GetAuthorizationCode with this:

 

        public static async Task<string> GetAuthorizationCode(IApiConfiguration config)
        {
            // Format the URL for the OAuth server login
            string url = string.Format("{0}?client_id={1}&redirect_uri={2}&scope={3}&response_type=code",
                                       CsOAuthServer, config.ClientId, HttpUtility.UrlEncode(config.RedirectUrl), CsOAuthScope);

            // Create a new form with WebView2
            var frm = new Form();
            var webView2 = new WebView2
            {
                Dock = DockStyle.Fill
            };
            frm.Controls.Add(webView2);

            // Set up a TaskCompletionSource to signal when the form can close
            var tcs = new TaskCompletionSource<string>();

            // Subscribe to NavigationCompleted to capture the OAuth redirect
            webView2.CoreWebView2InitializationCompleted += (sender, args) =>
            {
                if (args.IsSuccess)
                {
                    // Navigate to the OAuth login URL
                    webView2.CoreWebView2.Navigate(url);
                }
            };

            // Capture the URL in NavigationStarting
            webView2.NavigationStarting += (sender, args) =>
            {
                // Store the URI when navigation starts
                _currentUri = args.Uri;
            };

            // Use NavigationCompleted to check if the URI contains the authorization code
            webView2.NavigationCompleted += (sender, args) =>
            {
                if (_currentUri.Contains("code="))
                {
                    // Extract the authorization code from the URL
                    string code = ExtractAuthorizationCode(_currentUri);

                    // Signal the TaskCompletionSource that the form can close with the code
                    tcs.SetResult(code);

                    // Close the form
                    frm.Invoke((MethodInvoker)(() => frm.Close()));
                }
            };

            // Initialize WebView2 control asynchronously
            await webView2.EnsureCoreWebView2Async(null);

            frm.Size = new Size(800, 600);
            frm.Show();

            // Wait until the TaskCompletionSource is signaled (form is closed)
            string authCode = await tcs.Task;

            return authCode;

        }

 

Notes:

 

1. Change the call to the updated method to include an await:

_oAuthKeyService.OAuthResponse = oauthService.GetTokens(await   OAuthLogin.GetAuthorizationCode(_configurationCloud));

 

2. Make the containing method async

private async void Login()

 

3. You will probably get a brief flash of 'Failed navigation' as the http://desktop redirect page is shown before the form drops away. Maybe someone can sort that. Ironically, I don't need this code any more based on what I have been forced to learn this weekend.

38 Replies

  • Steve_PP's avatar
    Steve_PP
    Experienced User
    1 year ago

    My test app is .NET 4.7.2 and is WinForms based. If you are using WinForms, you need this namespace:

     
    using Microsoft.Web.WebView2.WinForms;

  • Digiwise's avatar
    Digiwise
    Trusted User
    1 year ago

    Hi Steve and other developers.

     

    This thread was very helpful in getting my solution running again.  Thank you for sharing the helpful information!

     

    Just a note that I ran into a problem when my app was installed in the Program Files (x86) folder.  WebView2 needs to create a user data folder and it can't create that folder under Program Files.  The version I am using doesn't give an error message when it can't create the folder - it just didn't display the browser.

     

    I found the fix at https://github.com/MicrosoftEdge/WebView2Feedback/issues/297 - there is code there to set the data folder:

     

    public BrowserForm(string url = null)
            {
                InitializeComponent();
                InitializeBrowser(url);
    
            }
    private async void InitializeBrowser(string url = null)
            {
                var userDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\SoftwareName";
                var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
                await webView21.EnsureCoreWebView2Async(env);
                webView21.Source = new UriBuilder(url ?? Settings.Default.HomePage).Uri;
            }

     

    Hope this helps!

     

    Clancy

  • eJulia's avatar
    eJulia
    Trusted User
    1 year ago

    Hi Steve,

     

    I do not use the sdk as we created private version to integrate it withour error logging back in 2013 when we implemented the first of our apps. I am trying to implement your code right now and striking a problem that it does not recognise WebView2.CoreWebView2InitializationCompleted 

     

    I have downloaded and installed the nuget package Microsoft.Web,WebView2 and by referencing it and inserting "using WebView2 = Microsoft.Web.WebView2;" in an appropriate spot it accepted some of your code.

     

    I earlier updated from .NET Framework 4.6.2 to 4.7.2 in a desperate hope that would make this problem go away when it first appeared. The WebView2 I downloaded appears to target 4.6.2 or better.

     

    Is there some other thing I need to download/reference/using to make thi swork.

     

    It is urgent. We have over 100 users most of whom use the online version. If they need to reconfigure to a new file or install on a new PC they are in trouble. Any potential new user trying out the 30 day trial is going to walk away in disgust until we have this fixed. 

  • Steve_PP's avatar
    Steve_PP
    Experienced User
    1 year ago

    Just to complete this thread, the issue with new app keys is that you can only create 2 without getting MYOB involved to manually activate them.

  • Trish_Lee's avatar
    Trish_Lee
    Experienced User
    1 year ago

    Hi Steve_PP 

     

    It is all working ok as we don't have a new app and are using the SDK.

    We are just investigating what other WebView files we need to include for the client installation and then we should be good to go with updating our clients which will be an enormous relief.

    You have been extremely generous, and I cannot thank you enough.

    Trish

     

  • Steve_PP's avatar
    Steve_PP
    Experienced User
    1 year ago

    Good to hear Trish.

     

    I think there is still a problem registering new apps and have sent a query to the the API team. In summary, the auth process works, you get a code and with that can successfully retrieve OAuth access and refresh tokens but you end up with a 'Forbidden' response when trying to use the access token enumerate companies (either manually or via the SDK.)

     

    However, if you have an older app key/secret (like from about 2 weeks ago) everything seems to work fine.

  • Trish_Lee's avatar
    Trish_Lee
    Experienced User
    1 year ago

    Hi Steve,

    You are an absolutely amazing person, we owe you an enormous amount of respect and gratitude, we now have our app working again.

    Thank you 

    Trish

  • Steve_PP's avatar
    Steve_PP
    Experienced User
    1 year ago

    Hi Trish, sure.

     

            // Helper function to extract the authorization code from the URL
            private static string ExtractAuthorizationCode(string uri)
            {
                var uriObj = new Uri(uri);
                var queryParams = HttpUtility.ParseQueryString(uriObj.Query);
                return queryParams["code"];
            }

     

    Here's the whole class:

     

        public static class OAuthLogin
        {
            private const string CsOAuthServer = "https://secure.myob.com/oauth2/account/authorize/";
            private const string CsOAuthScope = "CompanyFile";
            private static string _currentUri = string.Empty;

            public static async Task<string> GetAuthorizationCode(IApiConfiguration config)
            {
                // Format the URL for the OAuth server login
                string url = string.Format("{0}?client_id={1}&redirect_uri={2}&scope={3}&response_type=code",
                                           CsOAuthServer, config.ClientId, HttpUtility.UrlEncode(config.RedirectUrl), CsOAuthScope);

                // Create a new form with WebView2
                var frm = new Form();
                var webView2 = new WebView2
                {
                    Dock = DockStyle.Fill
                };
                frm.Controls.Add(webView2);

                // Set up a TaskCompletionSource to signal when the form can close
                var tcs = new TaskCompletionSource<string>();

                // Subscribe to NavigationCompleted to capture the OAuth redirect
                webView2.CoreWebView2InitializationCompleted += (sender, args) =>
                {
                    if (args.IsSuccess)
                    {
                        // Navigate to the OAuth login URL
                        webView2.CoreWebView2.Navigate(url);
                    }
                };

                // Capture the URL in NavigationStarting
                webView2.NavigationStarting += (sender, args) =>
                {
                    // Store the URI when navigation starts
                    _currentUri = args.Uri;
                };

                // Use NavigationCompleted to check if the URI contains the authorization code
                webView2.NavigationCompleted += (sender, args) =>
                {
                    if (_currentUri.Contains("code="))
                    {
                        // Extract the authorization code from the URL
                        string code = ExtractAuthorizationCode(_currentUri);

                        // Signal the TaskCompletionSource that the form can close with the code
                        tcs.SetResult(code);

                        // Close the form
                        frm.Invoke((MethodInvoker)(() => frm.Close()));
                    }
                };

                // Initialize WebView2 control asynchronously
                await webView2.EnsureCoreWebView2Async(null);

                frm.Size = new Size(800, 600);
                frm.Show();

                // Wait until the TaskCompletionSource is signaled (form is closed)
                string authCode = await tcs.Task;

                return authCode;

            }

            // Helper function to extract the authorization code from the URL
            private static string ExtractAuthorizationCode(string uri)
            {
                var uriObj = new Uri(uri);
                var queryParams = HttpUtility.ParseQueryString(uriObj.Query);
                return queryParams["code"];
            }

        }

  • Trish_Lee's avatar
    Trish_Lee
    Experienced User
    1 year ago

    Hi Steve_PP ,

    Can I bother you for the 

    ExtractAuthorizationCode proceedure, it would help enormously.

     

    Thanks

    Trish