<|start_filename|>Shared/WebViewTraceListener.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Microsoft.Toolkit.Win32.UI.Controls;
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
using Newtonsoft.Json;

namespace WebViewAddAllowedWebObjectWorkaround.Shared
{
    /// <summary>
    /// Provides a class to send debug and trace output to the sample page's logger
    /// </summary>
    /// <seealso cref="System.Diagnostics.TraceListener" />
    public class WebViewTraceListener : TraceListener
    {
        private static readonly string CSharpCategory = FormatCategory("C#");
        private readonly IWebView _webView;

        /// <summary>
        /// Initializes a new instance of the <see cref="WebViewTraceListener"/> class.
        /// </summary>
        /// <param name="webView">The web view.</param>
        /// <exception cref="ArgumentNullException">webView if null</exception>
        public WebViewTraceListener(IWebView webView)
        {
            _webView = webView ?? throw new ArgumentNullException(nameof(webView));
        }

        /// <summary>
        /// When overridden in a derived class, writes the specified message to the listener you create in the derived class.
        /// </summary>
        /// <param name="message">A message to write.</param>
        /// <seealso cref="WriteLine(string)"/>
        public override void Write(string message)
        {
            WriteLine(message);
        }

        /// <summary>
        /// Writes a category name and the value of the object's <see cref="M:System.Object.ToString" /> method to the listener you create when you implement the <see cref="T:System.Diagnostics.TraceListener" /> class.
        /// </summary>
        /// <param name="o">An <see cref="T:System.Object" /> whose fully qualified class name you want to write.</param>
        /// <param name="category">A category name used to organize the output.</param>
        /// <seealso cref="WriteLine(object, string)"/>
        public override void Write(object o, string category)
        {
            WriteLine(o, category);
        }

        /// <summary>
        /// Writes a category name and a message to the listener you create when you implement the <see cref="T:System.Diagnostics.TraceListener" /> class.
        /// </summary>
        /// <param name="message">A message to write.</param>
        /// <param name="category">A category name used to organize the output.</param>
        /// <seealso cref="WriteLine(string, string)"/>
        public override void Write(string message, string category)
        {
            WriteLine(message, category);
        }

        /// <summary>
        /// Writes the value of the object's <see cref="M:System.Object.ToString" /> method to the listener you create when you implement the <see cref="T:System.Diagnostics.TraceListener" /> class.
        /// </summary>
        /// <param name="o">An <see cref="T:System.Object" /> whose fully qualified class name you want to write.</param>
        /// <seealso cref="Write(object, string)"/>
        public override void Write(object o)
        {
            Write(o, string.Empty);
        }

        /// <summary>
        /// When overridden in a derived class, writes a message to the listener you create in the derived class, followed by a line terminator.
        /// </summary>
        /// <param name="message">A message to write.</param>
        /// <seealso cref="Eval(string, string, object)"/>
        public override void WriteLine(string message)
        {
            Eval(null, message);
        }

        /// <summary>
        /// Writes a category name and a message to the listener you create when you implement the <see cref="T:System.Diagnostics.TraceListener" /> class, followed by a line terminator.
        /// </summary>
        /// <param name="message">A message to write.</param>
        /// <param name="category">A category name used to organize the output.</param>
        /// <seealso cref="Eval(string, string, object)"/>
        public override void WriteLine(string message, string category)
        {
            Eval(category, message);
        }

        /// <summary>
        /// Writes the value of the object's <see cref="M:System.Object.ToString" /> method to the listener you create when you implement the <see cref="T:System.Diagnostics.TraceListener" /> class, followed by a line terminator.
        /// </summary>
        /// <param name="o">An <see cref="T:System.Object" /> whose fully qualified class name you want to write.</param>
        /// <seealso cref="WriteLine(object, string)"/>
        public override void WriteLine(object o)
        {
            WriteLine(o, string.Empty);
        }

        /// <summary>
        /// Writes a category name and the value of the object's <see cref="M:System.Object.ToString" /> method to the listener you create when you implement the <see cref="T:System.Diagnostics.TraceListener" /> class, followed by a line terminator.
        /// </summary>
        /// <param name="o">An <see cref="T:System.Object" /> whose fully qualified class name you want to write.</param>
        /// <param name="category">A category name used to organize the output.</param>
        /// <seealso cref="Eval(string, string, object)"/>
        public override void WriteLine(object o, string category)
        {
            Eval(category, null, o);
        }

        /// <summary>
        /// Formats the category into HTML for the logger.
        /// </summary>
        /// <param name="category">The category.</param>
        /// <returns>HTML for the <paramref name="category"/>.</returns>
        private static string FormatCategory(string category)
        {
            return string.IsNullOrWhiteSpace(category)
                ? string.Empty
                : $"<span class=\"badge\">{category}</span>";
        }

        private void Eval(string category, string message, object data = null)
        {
            var categories = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                CSharpCategory,
                FormatCategory(category)
            };

            var m = new StringBuilder();

            foreach (var c in categories)
            {
                m.Append(c);
                m.Append(" ");
            }

            m.Append(message?.Replace("'", "\\'") ?? string.Empty);

            var args = new List<object>(2)
            {
                // We always have a message
                m.Length > 0 ? m.ToString() : string.Empty
            };

            if (data != null) args.Add(data);

            var builder2 = new StringBuilder();
            builder2.Append("if (window && window.log) {");
            builder2.AppendFormat("  log('{0}'", args[0]);
            if (args.Count == 2)
            {
                builder2.Append(", ");
                builder2.Append(JsonConvert.SerializeObject(args[1],
                    JavaScriptBridgeMessageExtensions.MessageSerializationSettings));
            }

            builder2.AppendLine(");");
            builder2.Append("}");

            _webView.TryEvalAsync(builder2.ToString());
        }
    }
}

<|start_filename|>JavaScriptBridge/JavaScriptBridgeMessageExtensions.cs<|end_filename|>
﻿using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions
{
    /// <summary>
    /// Contains extension methods for <see cref="JavaScriptBridgeMessage"/>.
    /// </summary>
    public static class JavaScriptBridgeMessageExtensions
    {
        /// <summary>
        /// Serialization settings for Newtonsoft JSON.Net to produce JSON that JavaScript will be able to parse
        /// </summary>
        public static JsonSerializerSettings MessageSerializationSettings { get; } = new JsonSerializerSettings
        {
            NullValueHandling = NullValueHandling.Ignore,
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            StringEscapeHandling = StringEscapeHandling.EscapeHtml,
            Formatting = Formatting.None
        };

        /// <summary>
        /// Serializes the specified <paramref name="javaScriptBridgeMessage"/> to JSON.
        /// </summary>
        /// <param name="javaScriptBridgeMessage">A <see cref="JavaScriptBridgeMessage"/>.</param>
        /// <returns>JSON representation of the <paramref name="javaScriptBridgeMessage"/></returns>
        public static string Serialize(this JavaScriptBridgeMessage javaScriptBridgeMessage)
        {
            return JsonConvert.SerializeObject(javaScriptBridgeMessage, MessageSerializationSettings);
        }
    }
}


<|start_filename|>JavaScriptBridge/Constants.cs<|end_filename|>
﻿namespace Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions
{
    internal static class Constants
    {
        internal const string BridgeJavaScript = "Bridge.js";
        internal const string TraceName = "JavaScript Bridge";
    }
}


<|start_filename|>JavaScriptBridge/JavaScriptBridgeMessage.cs<|end_filename|>
﻿using System.Collections.Generic;

namespace Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions
{
    /// <summary>
    /// A data contract to move messages on the JavaScript bridge between JavaScript and .NET
    /// </summary>
    public class JavaScriptBridgeMessage
    {
        /// <summary>
        /// Gets or sets the callback.
        /// </summary>
        /// <value>The callback identifier to be used when the message is passed back to the JavaScript bridge.</value>
        /// <remarks>This is used internally to ensure the proper JavaScript callbacks are invoked when resuming execution within the JavaScript bridge.</remarks>
        public string CallbackId { get; set; }

        /// <summary>
        /// Gets or sets the error data.
        /// </summary>
        /// <value>The error data.</value>
        /// <remarks>This property is only set when an error occurs while processing the message.</remarks>
        public JavaScriptBridgeException ErrorData { get; set; }

        /// <summary>
        /// Gets or sets the handler to be used to process the message.
        /// </summary>
        /// <value>The handler.</value>
        /// <seealso cref="JavaScriptBridge.ScriptingDelegates"/>
        public string Handler { get; set; }

        /// <summary>
        /// Gets or sets the parameters to be used by the <see cref="Handler"/>.
        /// </summary>
        /// <value>The named parameters as <see cref="Dictionary{String,Object}"/>.</value>
        public Dictionary<string,object> HandlerData { get; set; }

        /// <summary>
        /// Gets or sets the response data.
        /// </summary>
        /// <value>An optional return object to be used by the JavaScript callback as a result of invoking the <see cref="Handler"/>.</value>
        public object ResponseData { get; set; }
    }
}


<|start_filename|>Shared/Constants.cs<|end_filename|>
﻿using System;
using Microsoft.Toolkit.Win32.UI.Controls;
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;

namespace WebViewAddAllowedWebObjectWorkaround.Shared
{
    /// <summary>
    /// Contains constant values for sample
    /// </summary>
    public static class Constants
    {
        /// <summary>
        /// URI of permissable <see cref="IWebView.ScriptNotify"/> events.
        /// </summary>
        /// <seealso cref="JavaScriptBridge.AllowedScriptNotifyUris"/>
        public static readonly Uri PermittedOrigin = new Uri(
            "ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Example.html");
    }
}


<|start_filename|>JavaScriptBridge/JavaScriptBridgeException.cs<|end_filename|>
﻿using System;

namespace Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions
{
    /// <summary>
    /// Represents errors that my occur while processing a <see cref="JavaScriptBridgeMessage"/>.
    /// </summary>
    /// <seealso cref="System.Exception" />
    public class JavaScriptBridgeException : Exception
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="JavaScriptBridgeException"/> class.
        /// </summary>
        /// <param name="message">The message that describes the error.</param>
        public JavaScriptBridgeException(string message)
            : base(message)
        {
        }
    }
}


<|start_filename|>JavaScriptBridge/WebViewExtensions.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions
{
    /// <summary>
    ///     Contains extension methods for <see cref="IWebView" />
    /// </summary>
    public static class WebViewExtensions
    {
        /// <summary>
        ///     Dispatch a <see cref="JavaScriptBridgeMessage" />.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="javaScriptBridgeMessage">The message.</param>
        internal static void DispatchMessage(this IWebView webView, JavaScriptBridgeMessage javaScriptBridgeMessage)
        {
            var json = javaScriptBridgeMessage?.Serialize();

            if (!string.IsNullOrEmpty(json))
            {
                Trace.WriteLine(
                    $"Calling {Constants.BridgeJavaScript} JavaScriptBridge.handleNativeMessage",
                    Constants.TraceName);
                Trace.WriteLine(javaScriptBridgeMessage, Constants.TraceName);

                // Need to sanitize message sent to bridge
                
                // Escape any single quotes, since that's what we're using
                json = json.Replace("'", "\\'");

                // Escape any CL/LF
                json = json.Replace(Environment.NewLine, "\\\\n");

                json = json.Replace("\n", "\\\\n");

                var command = $"JavaScriptBridge.handleNativeMessage('{json}');";
                // in WPF ensure UI access
                webView.TryEvalAsync(command);
            }
        }

        /// <summary>
        ///     Performs asynchronous JavaScript eval without waiting for return.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="script">The script to evaluate.</param>
        internal static void Eval(this IWebView webView, string script)
        {
            webView?.EvalAsync(script);
        }

        /// <summary>
        ///     Performs asynchronous JavaScript eval using <see cref="IWebView.InvokeScriptAsync(string, string[])" />.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="script">The script to evaluate.</param>
        /// <returns>A <see cref="Task{String}" />.</returns>
        internal static Task<string> EvalAsync(this IWebView webView, string script)
        {
            return webView?.InvokeScriptAsync("eval", script);
        }

        /// <summary>
        ///     Executes the given JavaScript function within the "window" scope. If the JavaScript function returns a value, it is
        ///     returned as <see cref="JToken" />.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        /// <returns>A <see cref="JToken" />.</returns>
        [DebuggerStepThrough]
        public static Task<JToken> InvokeJavaScriptFunctionWithJTokenResponse(
            this IWebView webView,
            string function,
            params object[] args)
        {
            return InvokeJavaScriptFunctionWithJTokenResponse(webView, "window", function, args);
        }

        /// <summary>
        ///     Executes the given JavaScript function within the specified scope. If the JavaScript function returns a value, it
        ///     is returned as <see cref="JToken" />.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="module">The JavaScript module (e.g. window).</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        /// <returns>A <see cref="JToken" />.</returns>
        public static async Task<JToken> InvokeJavaScriptFunctionWithJTokenResponse(
            IWebView webView,
            string module,
            string function,
            params object[] args)
        {
            var result = await InvokeScriptFunctionAsync(webView, module, function, args);
            return JsonConvert.DeserializeObject<JToken>(result);
        }

        /// <summary>
        ///     Executes the given JavaScript function within the "window" scope. If the JavaScript function returns a value, it is
        ///     returned as JSON.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        /// <returns>If the function returns a value, JSON representation of that value.</returns>
        /// <seealso cref="InvokeScriptFunctionAsync(IWebView,string,string,object[])" />
        [DebuggerStepThrough]
        public static Task<string> InvokeScriptFunctionAsync(this IWebView webView, string function,
            params object[] args)
        {
            return InvokeScriptFunctionAsync(webView, "window", function, args);
        }

        /// <summary>
        ///     Executes the given JavaScript function in the specified module with arguments. If the JavaScript function returns a
        ///     value, it is returned as JSON.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="module">The JavaScript module (e.g. window).</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        /// <returns>If the function returns a value, JSON representation of that value.</returns>
        public static Task<string> InvokeScriptFunctionAsync(this IWebView webView, string module, string function,
            params object[] args)
        {
            var builder = new StringBuilder();
            builder.AppendLine("(function() {");
            builder.AppendFormat("  if ({0} && {0}.{1}) {{\r\n", module, function);
            builder.AppendFormat("    return JSON.stringify({0}.{1}(", module, function);

            args = args ?? Array.Empty<object>();

            var builder2 = new StringBuilder();
            var i = 0;
            while (true)
            {
                if (i >= args.Length)
                {
                    builder.Append(builder2);
                    builder.AppendLine("));");
                    builder.AppendLine("  }");
                    builder.Append("})();");

                    break;
                }

                if (i > 0)
                {
                    builder.Append(", ");
                }

                builder2.Append(
                    JsonConvert.SerializeObject(
                        args[i],
                        JavaScriptBridgeMessageExtensions.MessageSerializationSettings));
                i++;
            }

            return EvalAsync(webView, builder.ToString());
        }

        /// <summary>
        ///     Executes the given JavaScript function within the "window" scope. If the JavaScript function returns a value, it is
        ///     returned as an instance of
        ///     <typeparamref name="T" />
        ///     .
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        /// <typeparam name="T"></typeparam>
        /// <returns>
        ///     An instance of      <typeparamref name="T" />
        ///     .
        /// </returns>
        [DebuggerStepThrough]
        public static Task<T> InvokeScriptFunctionAsync<T>(
            this IWebView webView,
            string function,
            params object[] args)
        {
            return InvokeScriptFunctionAsync<T>(webView, "window", function, args);
        }

        /// <summary>
        ///     Executes the given JavaScript function within the specified module. If the JavaScript function returns a value, it
        ///     is returned as an instance of
        ///     <typeparamref name="T" />
        ///     .
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="module">The JavaScript module (e.g. window).</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        /// <typeparam name="T"></typeparam>
        /// <returns>
        ///     An instance of
        ///     <typeparamref name="T" />
        ///     .
        /// </returns>
        public static async Task<T> InvokeScriptFunctionAsync<T>(
            this IWebView webView,
            string module,
            string function,
            params object[] args)
        {
            var result = await InvokeScriptFunctionAsync(webView, module, function, args);

            if (string.IsNullOrEmpty(result))
            {
                return default(T);
            }

            try
            {
                return JsonConvert.DeserializeObject<T>(result);
            }
            catch (JsonReaderException)
            {
                throw;
            }

            return default(T);
        }

        /// <summary>
        ///     Executes the given JavaScript function within the "window" scope.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        [DebuggerStepThrough]
        public static Task<Dictionary<string, object>> InvokeScriptFunctionWithDicionaryResponseAsync(
            this IWebView webView,
            string function,
            params object[] args)
        {
            return InvokeScriptFunctionWithDicionaryResponseAsync(webView, "window", function, args);
        }

        /// <summary>
        ///     Executes the given JavaScript function within the specified scope.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="module">The JavaScript module (e.g. window).</param>
        /// <param name="function">The name of the JavaScript function in the module.</param>
        /// <param name="args">The arguments to pass to the function.</param>
        public static async Task<Dictionary<string, object>> InvokeScriptFunctionWithDicionaryResponseAsync(
            this IWebView webView,
            string module,
            string function,
            params object[] args)
        {
            var result = await InvokeScriptFunctionAsync(webView, module, function, args);
            return !string.IsNullOrEmpty(result)
                ? JsonConvert.DeserializeObject<Dictionary<string, object>>(result)
                : new Dictionary<string, object>(0);
        }

        /// <summary>
        ///     Attempts to asynchronously evaluate the <paramref name="script" /> in the <paramref name="webView" /> without
        ///     waiting for return.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <param name="script">The script to evaluate.</param>
        public static void TryEvalAsync(this IWebView webView, string script)
        {
            if (webView != null)
            {
                try
                {
                    webView.Eval(script);
                }
                catch (Exception e)
                {
                    // Best effort
                    Debug.WriteLine("Error executing command: " + script);
                    Debug.WriteLine(e.Message);
                }
            }
        }
    }
}

<|start_filename|>WebViewAddAllowedWebObjectWorkaround/Form1.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Windows.Forms;
using Microsoft.Toolkit.Win32.UI.Controls;
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
using WebViewAddAllowedWebObjectWorkaround.Shared;

namespace WebViewAddAllowedWebObjectWorkaround
{
    public partial class Form1 : Form
    {
        private readonly JavaScriptBridge _javaScriptBridge;
        private readonly ProductRepository _productRepository;

        public Form1()
        {
            InitializeComponent();

            // Initialize the product repository
            _productRepository = new ProductRepository();

            // Initialize the JavaScriptBridge
            _javaScriptBridge = JavaScriptBridge.CreateAndStart(webView1, Constants.PermittedOrigin);
            _javaScriptBridge.AddScriptingHandler("GetAllProducts", @params => _productRepository.GetProducts());
            _javaScriptBridge.AddScriptingHandler("GetProduct", @params =>
            {
                // Since these are coming from JavaScript, take extra care to validate the parameters
                if (@params != null && @params.Count == 1 && @params.ContainsKey("name"))
                {
                    var name = @params["name"]?.ToString();
                    if (!string.IsNullOrEmpty(name))
                    {
                        return _productRepository.GetProductByName(name);
                    }
                }

                throw new ArgumentException("Unexpected parameters");
            });

            webView1.DOMContentLoaded += (o, e) => Text = webView1.DocumentTitle;

            // This is a custom text writer trace listener to send diagnostic data to the webview log
            Trace.Listeners.Add(new WebViewTraceListener(webView1));

            // When the page loads push the product data to the page and bind with JavaScript showData function
            webView1.NavigationCompleted += (o, e) =>
            {
                if (o is IWebView wv)
                {
                    // Get the products and push to JavaScript function "showData"
                    var products = new ProductRepository().GetProducts();
                    wv.InvokeScriptFunctionAsync("showData", products);
                }
            };

            // Perform the navigation as you normally would. This could either by through the Source property,
            // or through one of the navigation methods.
            //
            // Using the method to navigate to local content for our example
            webView1.NavigateToLocal("/Example.html");
        }
    }
}


<|start_filename|>Shared/ProductRepository.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace WebViewAddAllowedWebObjectWorkaround.Shared
{
    /// <summary>
    /// A sample data provider, providing instancess of <see cref="Product"/>.
    /// </summary>
    public class ProductRepository
    {
        private static readonly Dictionary<string, Product> Products = new Dictionary<string, Product>(StringComparer.OrdinalIgnoreCase)
        {
            {"Apple", new Product
                {
                    Name = "Apple",
                    ExpiryDate = DateTime.Today.AddDays(3d),
                    Price = 3.99M,
                    Sizes = new []{"Small", "Medium", "Large"}
                }
            },
            {"Pear", new Product
                {
                    Name = "Pear",
                    ExpiryDate = DateTime.Today.AddDays(7d),
                    Price = 2.29M,
                    Sizes = new []{"Small", "Large"}
                }
            }
        };

        /// <summary>
        /// Gets the products.
        /// </summary>
        /// <returns><seealso cref="IEnumerable{Product}"/></returns>
        public IEnumerable<Product> GetProducts()
        {
            Trace.WriteLine($"Entering {GetType().FullName}.{nameof(GetProducts)}");
            return Products.Values;
        }

        /// <summary>
        /// Gets the <see cref="Product"/> for a given name.
        /// </summary>
        /// <param name="name">The name of the product.</param>
        /// <returns>If the repository contains a product with the given name, then an instance of <see cref="Product"/>; otherwise, null.</returns>
        public Product GetProductByName(string name)
        {
            Trace.WriteLine($"Entering {GetType().FullName}.{nameof(GetProductByName)}");
            Products.TryGetValue(name, out var retval);
            return retval;
        }
    }
}

<|start_filename|>WebViewAddAllowedWebObjectWorkaround_WPF/MainWindow.xaml.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Windows;
using Microsoft.Toolkit.Win32.UI.Controls;
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
using WebViewAddAllowedWebObjectWorkaround.Shared;


namespace WebViewAddAllowedWebObjectWorkaround_WPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly JavaScriptBridge _javaScriptBridge;
        private readonly ProductRepository _productRepository;

        public MainWindow()
        {
            InitializeComponent();

            // Initialize the product repository
            _productRepository = new ProductRepository();

            // Initialize the JavaScriptBridge
            _javaScriptBridge = JavaScriptBridge.CreateAndStart(WebView, Constants.PermittedOrigin);
            _javaScriptBridge.AddScriptingHandler("GetAllProducts", @params => _productRepository.GetProducts());
            _javaScriptBridge.AddScriptingHandler("GetProduct", @params =>
            {
                // Since these are coming from JavaScript, take extra care to validate the parameters
                if (@params != null && @params.Count == 1 && @params.ContainsKey("name"))
                {
                    var name = @params["name"]?.ToString();
                    if (!string.IsNullOrEmpty(name))
                    {
                        return _productRepository.GetProductByName(name);
                    }
                }

                throw new ArgumentException("Unexpected parameters");
            });

            WebView.DOMContentLoaded += (o, e) => Title = WebView.DocumentTitle;

            // This is a custom text writer trace listener to send diagnostic data to the webview log
            Trace.Listeners.Add(new WebViewTraceListener(WebView));


            // When the page loads push the product data to the page and bind with JavaScript showData function
            WebView.NavigationCompleted += (o, e) =>
            {
                if (o is IWebView wv)
                {
                    // Get the products and push to JavaScript function "showData"
                    var products = new ProductRepository().GetProducts();
                    wv.InvokeScriptFunctionAsync("showData", products);
                }
            };
        }

        private void WebView_OnLoaded(object sender, RoutedEventArgs e)
        {
            // Perform the navigation as you normally would. This could either by through the Source property,
            // or through one of the navigation methods.
            //
            // Using the method to navigate to local content for our example
            WebView.NavigateToLocal("/Example.html");
        }
    }
}


<|start_filename|>JavaScriptBridge/Bridge.js<|end_filename|>
﻿(function() {
    if (window.JavaScriptBridge) {
        return;
    }

    var sendMessageQueue = [],
        responseCallbacks = {},
        errorCallbacks = {},
        messagingFrame,
        msgCnt = 1;

    function handleNativeMessage(message) {
            dispatchNativeMessage(message);
    }

    function dispatchNativeMessage(messageJSON) {
        setTimeout(function _timeout() {
            const message = JSON.parse(jsonEscape(messageJSON));
            var responseCallback, errorCallback;

            // Check if some type of callback was specified
            if (message.callbackId) {
                // If there was an exception there will be data in message.errorData
                // We either have a callback for resolve or reject
                // Either way, we need to delete the appropriate callbacks

                if (message.errorData) {
                    // We have an Exception, delete the resolve callback and invoke the reject
                    responseCallback = responseCallbacks[message.callbackId];
                    if (responseCallback) {
                        delete responseCallbacks[message.callbackId];
                    }

                    errorCallback = errorCallbacks[message.callbackId];
                    if (errorCallback) {
                        errorCallback(message.errorData);
                        delete errorCallbacks[message.callbackId];
                    }
                } else {
                    errorCallback = errorCallbacks[message.callbackId];
                    if (errorCallback) {
                        delete errorCallbacks[message.callbackId];
                    }

                    responseCallback = responseCallbacks[message.callbackId];
                    if (responseCallback) {
                        responseCallback(message.responseData);
                        delete responseCallbacks[message.callbackId];
                    }
                }
            }
        });
    }

    function fetchQueue() {
        const queue = sendMessageQueue;
        sendMessageQueue = [];
        return queue;
    }

    function send(data, onSuccess, onFailure) {
        callNative(null, data, onSuccess, onFailure);
    }

    function callNative(handler, data, onSuccess, onFailure) {
        const func = this.callNative;

        data = data || {};
        if (typeof data === "function") {
            // No data specified. Need to shift params over by one
            onFailure = onSuccess;
            onSuccess = data;
            data = null;
        }

        if (onSuccess === undefined && onFailure === undefined) {
            return new Promise(function(resolve, reject) {
                func(handler,
                    data,
                    function(result) {
                        resolve(result);
                    },
                    function(err) {
                        reject(err);
                    });
            });
        }

        return sendImpl(
            { handler: handler, handlerdata: data },
            onSuccess,
            onFailure);
    }

    function sendImpl(message, onSuccess, onFailure) {
        if (onSuccess !== undefined || onFailure !== undefined) {
            const cbid = `cb_${msgCnt++}_${new Date().getTime()}`;

            message["callbackId"] = cbid;

            if (onSuccess) {
                responseCallbacks[cbid] = onSuccess;
            }

            if (onFailure) {
                errorCallbacks[cbid] = onFailure;
            }
        }

        sendMessageQueue.push(message);
        const notifyMessage = "jsbridge://queue_message";

        if (messagingFrame) {
            messagingFrame.contentWindow.postMessage(notifyMessage, "*");
        } else {
            window.external.notify(notifyMessage);
        }
    }

    function jsonEscape(input) {
        return input
            .replace(/\n/g, "\\\\n")
            .replace(/\r/g, "\\\\r")
            .replace(/\t/g, "\\\\t");
    }

    window.JavaScriptBridge = {
        send: send,
        callNative: callNative,
        fetchQueue: fetchQueue,
        handleNativeMessage: handleNativeMessage
    };

    // iframe to handle window.external.notify
    // TODO: Detect if this is necessary for your scenario
    //messagingFrame = document.createElement('iframe');
    //messagingFrame.style.display = 'none';
    //messagingFrame.src = "/Bridge.html";
    //document.documentElement.appendChild(messagingFrame);

    const readyEvent = new CustomEvent("JavaScriptBridgeReady");
    readyEvent.bridge = JavaScriptBridge;
    document.dispatchEvent(readyEvent);
})();

<|start_filename|>JavaScriptBridge/JavaScriptBridge.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions
{
    /// <summary>
    /// A means of communicating bi-directionally with JavaScript on a web page.
    /// </summary>
    public class JavaScriptBridge
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="JavaScriptBridge"/> class.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView"/> instance.</param>
        /// <exception cref="ArgumentNullException">When <paramref name="webView"/> is null.</exception>
        public JavaScriptBridge(IWebView webView)
        {
            WebView = webView ?? throw new ArgumentNullException(nameof(webView));

            ScriptingDelegates = new Dictionary<string, Func<Dictionary<string, object>, object>>(StringComparer.OrdinalIgnoreCase);
            AllowedScriptNotifyUris = new HashSet<Uri>();

            // Ensure the following properties are set: We rely on JavaScript and ScriptNotify for the bridge to function
            WebView.IsJavaScriptEnabled = true;
            WebView.IsScriptNotifyAllowed = true;

            IsActive = false;
        }

        /// <summary>
        ///     Gets or sets a safe list of Uniform Resource Identifiers (URIs) that are permitted to fire
        ///     <see cref="IWebView.ScriptNotify" /> events to the <see cref="IWebView" /> for consumption by the
        ///     <see cref="JavaScriptBridge" />
        /// </summary>
        public ICollection<Uri> AllowedScriptNotifyUris { get; }

        /// <summary>
        /// Gets a value indicating whether this instance is actively listening to <see cref="IWebView.ScriptNotify"/> events..
        /// </summary>
        /// <value><see langword="true" /> if this instance is active; otherwise, <see langword="false" />.</value>
        public bool IsActive { get; private set; }

        /// <summary>
        /// Gets or sets the .NET delegates that may be invoked from JavaScript.
        /// </summary>
        /// <value>The scripting delegates.</value>
        protected Dictionary<string, Func<Dictionary<string, object>, object>> ScriptingDelegates { get; }

        /// <summary>
        /// Gets the <see cref="IWebView"/> instance.
        /// </summary>
        /// <value>The web view.</value>
        protected IWebView WebView { get; }

        /// <summary>
        /// Creates an instance of <see cref="JavaScriptBridge"/> and starts listening to <see cref="IWebView.ScriptNotify"/> messages.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView"/> instance.</param>
        /// <param name="allowedOrigins">The set of allowed origins for <see cref="IWebView.ScriptNotify"/> events.</param>
        /// <returns>An instance of <see cref="JavaScriptBridge" />.</returns>
        /// <exception cref="ArgumentNullException">
        /// Occurs when <paramref name="webView"/> or  <paramref name="allowedOrigins" /> is null.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">allowedOrigins - Must have at least one origin.</exception>
        /// <seealso cref="Start"/>
        /// <seealso cref="AllowedScriptNotifyUris"/>
        public static JavaScriptBridge CreateAndStart(
            IWebView webView,
            params Uri[] allowedOrigins
        )
        {

            if (allowedOrigins == null)
            {
                throw new ArgumentNullException(nameof(allowedOrigins));
            }

            if (!allowedOrigins.Any())
            {
                throw new ArgumentOutOfRangeException(nameof(allowedOrigins), "Must have at least one origin.");
            }

            var retval = new JavaScriptBridge(webView);

            foreach (var origin in allowedOrigins)
            {
                retval.AllowedScriptNotifyUris.Add(origin);
            }

            retval.Start();

            return retval;
        }

        /// <summary>
        /// Adds the scripting delegate with a given <paramref name="handlerId"/>.
        /// </summary>
        /// <param name="handlerId">The identity of the delegate to be used by JavaScript.</param>
        /// <param name="action">The action to be performed.</param>
        /// <remarks>
        /// The <paramref name="action"/> is a callback to be performed when invoking the delegate by id. The callback accepts a named parameter collection
        /// via <see cref="Dictionary{String,Object}"/> and permits a return of <see cref="Object"/>. The callback itself need not accept any parameters, nor
        /// need it return a value. If no parameters are passed, an empty <see cref="Dictionary{String,Object}"/> is passed for the parameters. If the callback
        /// does not produce a return value, <see langword="null" /> is returned.
        /// </remarks>
        public void AddScriptingHandler(string handlerId, Func<Dictionary<string, object>, object> action)
        {
            ScriptingDelegates.Add(handlerId, action);
        }

        /// <summary>
        /// When overridden in a class, provides custom logic to be executed after <see cref="Start"/>.
        /// </summary>
        /// <seealso cref="Start"/>.
        /// <remarks>This implementation ensures the bridge is loaded by handling <see cref="IWebView.DOMContentLoaded"/> and the
        /// <see cref="IWebView.Source"/> is a valid <see cref="AllowedScriptNotifyUris"/>.</remarks>
        public virtual void OnStarted()
        {
            WebView.DOMContentLoaded += OnWebViewDOMContentLoaded;
        }

        /// <summary>
        /// When overridden in a class, provides custom logic to be executed after <see cref="Stop"/>.
        /// </summary>
        /// <seealso cref="Stop"/>.
        /// <remarks>Stops listening for <see cref="IWebView.DOMContentLoaded"/>.</remarks>
        /// <seealso cref="OnStarted"/>
        public virtual void OnStopped()
        {
            WebView.DOMContentLoaded -= OnWebViewDOMContentLoaded;
        }

        /// <summary>
        /// Starts listening for <see cref="IWebView.ScriptNotify"/> events.
        /// </summary>
        /// <seealso cref="IWebView.ScriptNotify"/>
        /// <seealso cref="AllowedScriptNotifyUris"/>
        /// <seealso cref="IsActive"/>
        /// <seealso cref="OnStarted"/>
        public void Start()
        {
            if (!IsActive)
            {
                IsActive = true;

                WebView.ScriptNotify += OnWebViewScriptNotify;

                OnStarted();
            }
        }


        /// <summary>
        /// Stops listening for <see cref="IWebView.ScriptNotify"/> events.
        /// </summary>
        /// <seealso cref="IsActive"/>
        /// <seealso cref="IWebView.ScriptNotify"/>
        /// <seealso cref="OnStopped"/>
        public void Stop()
        {
            if (IsActive)
            {
                IsActive = false;

                WebView.ScriptNotify -= OnWebViewScriptNotify;

                OnStopped();
            }
        }

        /// <summary>
        /// Handle the <see cref="IWebView.DOMContentLoaded" /> event, ensuring the JavaScript bridge is loaded.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="WebViewControlDOMContentLoadedEventArgs"/> instance containing the event data.</param>
        /// <remarks>
        /// The bridge can be loaded by
        ///   - Explicitly using a script tag in the markup of the page, or
        ///   - Explicitly through handling the navigation or DOM events
        ///
        /// If the JavaScript bridge is already loaded, it will not be loaded again.
        /// </remarks>
        /// <seealso cref="AllowedScriptNotifyUris"/>
        /// <seealso cref="LoadBridge"/>
        private void OnWebViewDOMContentLoaded(object sender, WebViewControlDOMContentLoadedEventArgs e)
        {
            if (e.Uri != null &&
                AllowedScriptNotifyUris != null &&
                AllowedScriptNotifyUris.Contains(e.Uri) &&
                sender is IWebView webView)
            {
                LoadBridge();
            }
        }

        /// <summary>
        ///     Loads the JavaScript bridge into a <paramref name="webView" /> instance if not already loaded.
        /// </summary>
        /// <param name="webView">A <see cref="IWebView" /> instance.</param>
        /// <remarks>
        ///     Bridge is not loaded unless the navigation was successful.
        /// </remarks>
        public async Task LoadBridge()
        {
            Trace.WriteLine("Executing JavaScript to detect presence of JavaScriptBridge", Constants.TraceName);

            var result = await WebView.InvokeScriptAsync("eval", "typeof JavaScriptBridge == 'object'");
            if (!"true".Equals(result))
            {
                // NOT already loaded. Load from disk
                Trace.WriteLine("Could not detect JavaScriptBridge. Injecting script", Constants.TraceName);

                var contents = File.ReadAllText(Constants.BridgeJavaScript);
                WebView.Eval(contents);
            }
        }

        /// <summary>
        /// Handles the <see cref="IWebView.ScriptNotify" /> event.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="WebViewControlScriptNotifyEventArgs"/> instance containing the event data.</param>
        /// <remarks>
        /// Since there may be other uses of <code>window.external.notify</code> in use, this method checks to see if the
        /// message came from a URI within <see cref="AllowedScriptNotifyUris"/> and the bridge before processing it. If it did,
        /// it triggers processing of all queued messages within the bridge asynchronously.
        /// </remarks>
        /// <seealso cref="AllowedScriptNotifyUris"/>
        /// <seealso cref="IsJavaScriptBridgeMessage"/>
        /// <seealso cref="FlushAsync"/>
        private void OnWebViewScriptNotify(object sender, WebViewControlScriptNotifyEventArgs e)
        {
            Trace.WriteLine($"Entering {typeof(JavaScriptBridge).FullName}.{nameof(OnWebViewScriptNotify)}", Constants.TraceName);

            // White list
            // WinForms/WPF do not validate the origins for ScriptNotify like UWP does
            // This needs to be done manually for security
            if (e.Uri != null && AllowedScriptNotifyUris != null && AllowedScriptNotifyUris.Contains(e.Uri))
            {
                var message = e.Value;

                if (sender is IWebView webView && IsJavaScriptBridgeMessage(message))
                {
                    Trace.WriteLine($"Received command {message}", Constants.TraceName);

                    // Asynchronously flush the messages from the JavaScript bridge
                    FlushAsync();
                }
            }
            else
            {
                Trace.WriteLine($"{nameof(IWebView.ScriptNotify)} received from unknown origin {e.Uri?.Host}", Constants.TraceName);
            }
        }

        /// <summary>
        ///     Determines if the <paramref name="message" /> is from the JavaScript bridge.
        /// </summary>
        /// <param name="message">The contents of <see cref="WebViewControlScriptNotifyEventArgs.Value" />.</param>
        /// <returns><see langword="true" /> if message originated from JavaScript bridge; otherwise, <see langword="false" />.</returns>
        private bool IsJavaScriptBridgeMessage(string message)
        {
            if (!string.IsNullOrEmpty(message))
            {
                // If using the helper methods "sendData" and "callNative" a message will fall into this
                if (message.StartsWith("jsbridge", StringComparison.OrdinalIgnoreCase))
                {
                    if (message.IndexOf("queue_message", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        return true;
                    }
                }

                // If pushing a message directly, it will be this
                // Need to make sure in this case it is not malformed
                JToken token = null;

                try
                {
                    token = JsonConvert.DeserializeObject<JToken>(message);
                }
                catch (Exception)
                {
                }

                if (token?[nameof(JavaScriptBridgeMessage.Handler)] != null)
                {
                    var delegateName = token[nameof(JavaScriptBridgeMessage.Handler)].Value<string>();
                    if (!string.IsNullOrEmpty(delegateName))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Flushes all queued messages from the JavaScript bridge
        /// </summary>
        /// <returns>A <see cref="Task"/>.</returns>
        /// <exception cref="NotSupportedException">If the requested callback identifier is not in the list of <see cref="ScriptingDelegates"/>.</exception>
        /// <seealso cref="ScriptingDelegates"/>
        /// <remarks>
        /// If an exception is encountered while processing the handler it is captured and returned to the caller via <see cref="JavaScriptBridgeMessage.ErrorData"/>.
        /// Each message is handled independently and dispatched back to the bridge in the order in which they are received.
        /// </remarks>
        private async Task FlushAsync()
        {
            {
                Trace.WriteLine($"Calling {Constants.BridgeJavaScript} method JavaScriptBridge.fetchQueue()",
                    Constants.TraceName);

                List<JavaScriptBridgeMessage> messages;

                try
                {
                    messages = await WebView.InvokeScriptFunctionAsync<List<JavaScriptBridgeMessage>>(
                        "JavaScriptBridge",
                        "fetchQueue");
                }
                catch (Exception)
                {
                    // TODO: Catch a more specific exception here
                    throw;
                }

                if (messages != null)
                {
                    Trace.WriteLine($"Retrieved {messages.Count} message{(messages.Count != 1 ? "s" : string.Empty)}",
                        Constants.TraceName);

                    foreach (var message in messages)
                    {
                        try
                        {
                            var handlerName = message.Handler;
                            if (ScriptingDelegates.ContainsKey(handlerName))
                            {
                                message.ResponseData = ScriptingDelegates[handlerName](message.HandlerData);
                            }
                            else
                            {
                                throw new NotSupportedException($"Handler '{handlerName}' not supported.");
                            }
                        }
                        catch (Exception exception)
                        {
                            Trace.WriteLine(
                                $"Error while processing message: [{exception.GetType()}] {exception.Message}",
                                Constants.TraceName);

                            // Encountered an excpetion while processing the message
                            // Mask any potential disclosure issues by wrapping the exception as our own
                            var e =
                                new JavaScriptBridgeException(exception.Message)
                                {
                                    Source = Constants.TraceName
                                };
                            message.ErrorData = e;
                        }
                        finally
                        {
                            if (!string.IsNullOrEmpty(message.CallbackId))
                            {
                                WebView.DispatchMessage(message);
                            }
                        }
                    }
                }
            }
        }
    }
}

<|start_filename|>Shared/Product.cs<|end_filename|>
﻿using System;

namespace WebViewAddAllowedWebObjectWorkaround.Shared
{
    // A sample plain old CLR object (POCO)
    /// <summary>
    /// A sample plain old CLR object (POCO) for product information
    /// </summary>
    public class Product
    {

        /// <summary>
        /// Gets or sets the name.
        /// </summary>
        /// <value>The name.</value>
        public string Name { get; set; }
        /// <summary>
        /// Gets or sets the expiry date.
        /// </summary>
        /// <value>The expiry date.</value>
        public DateTime ExpiryDate { get; set; }
        /// <summary>
        /// Gets or sets the price.
        /// </summary>
        /// <value>The price.</value>
        public decimal Price { get; set; }
        /// <summary>
        /// Gets or sets the sizes.
        /// </summary>
        /// <value>The sizes.</value>
        public string[] Sizes { get; set; }
    }
}