From bc3d0eb330be5c7abe5dc25db71d21432ca44f0c Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Thu, 16 Jan 2025 22:42:07 +0800 Subject: [PATCH] Embed the HybridWebView.js * Convert the JS file into TypeScript * Embed the js file * Link the output into the sample/test apps --- Directory.Build.targets | 3 + .../Maui.Controls.Sample.csproj | 1 + .../HybridSamplePage/scripts/HybridWebView.js | 149 ---------- .../DeviceTests/Controls.DeviceTests.csproj | 1 + .../HybridTestRoot/scripts/HybridWebView.js | 149 ---------- src/Core/src/Core.csproj | 17 ++ .../Handlers/HybridWebView/HybridWebView.js | 190 +++++++++++++ .../Handlers/HybridWebView/HybridWebView.ts | 254 ++++++++++++++++++ 8 files changed, 466 insertions(+), 298 deletions(-) delete mode 100644 src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js delete mode 100644 src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/scripts/HybridWebView.js create mode 100644 src/Core/src/Handlers/HybridWebView/HybridWebView.js create mode 100644 src/Core/src/Handlers/HybridWebView/HybridWebView.ts diff --git a/Directory.Build.targets b/Directory.Build.targets index cdfa0ed38f55..143f356e234a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -125,4 +125,7 @@ + + + diff --git a/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj b/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj index 308ab6a0e930..087e5608e9c5 100644 --- a/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj +++ b/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj @@ -77,6 +77,7 @@ + diff --git a/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js b/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js deleted file mode 100644 index 134d7d062c3d..000000000000 --- a/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js +++ /dev/null @@ -1,149 +0,0 @@ -window.HybridWebView = { - "Init": function Init() { - function DispatchHybridWebViewMessage(message) { - const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); - window.dispatchEvent(event); - } - - if (window.chrome && window.chrome.webview) { - // Windows WebView2 - window.chrome.webview.addEventListener('message', arg => { - DispatchHybridWebViewMessage(arg.data); - }); - } - else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { - // iOS and MacCatalyst WKWebView - window.external = { - "receiveMessage": message => { - DispatchHybridWebViewMessage(message); - } - }; - } - else { - // Android WebView - window.addEventListener('message', arg => { - DispatchHybridWebViewMessage(arg.data); - }); - } - }, - - "SendRawMessage": function SendRawMessage(message) { - window.HybridWebView.__SendMessageInternal('__RawMessage', message); - }, - - "InvokeDotNet": async function InvokeDotNetAsync(methodName, paramValues) { - const body = { - MethodName: methodName - }; - - if (typeof paramValues !== 'undefined') { - if (!Array.isArray(paramValues)) { - paramValues = [paramValues]; - } - - for (var i = 0; i < paramValues.length; i++) { - paramValues[i] = JSON.stringify(paramValues[i]); - } - - if (paramValues.length > 0) { - body.ParamValues = paramValues; - } - } - - const message = JSON.stringify(body); - - var requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; - - const rawResponse = await fetch(requestUrl, { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - const response = await rawResponse.json(); - - if (response) { - if (response.IsJson) { - return JSON.parse(response.Result); - } - - return response.Result; - } - - return null; - }, - - "__SendMessageInternal": function __SendMessageInternal(type, message) { - - const messageToSend = type + '|' + message; - - if (window.chrome && window.chrome.webview) { - // Windows WebView2 - window.chrome.webview.postMessage(messageToSend); - } - else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { - // iOS and MacCatalyst WKWebView - window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend); - } - else { - // Android WebView - hybridWebViewHost.sendMessage(messageToSend); - } - }, - - "__InvokeJavaScript": async function __InvokeJavaScript(taskId, methodName, args) { - try { - var result = null; - if (methodName[Symbol.toStringTag] === 'AsyncFunction') { - result = await methodName(...args); - } else { - result = methodName(...args); - } - window.HybridWebView.__TriggerAsyncCallback(taskId, result); - } catch (ex) { - console.error(ex); - window.HybridWebView.__TriggerAsyncFailedCallback(taskId, ex); - } - }, - - "__TriggerAsyncFailedCallback": function __TriggerAsyncCallback(taskId, error) { - - if (!error) { - json = { - Message: "Unknown error", - StackTrace: Error().stack - }; - } else if (error instanceof Error) { - json = { - Name: error.name, - Message: error.message, - StackTrace: error.stack - }; - } else if (typeof (error) === 'string') { - json = { - Message: error, - StackTrace: Error().stack - }; - } else { - json = { - Message: JSON.stringify(error), - StackTrace: Error().stack - }; - } - - json = JSON.stringify(json); - - window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptFailed', taskId + '|' + json); - }, - - "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) { - // Make sure the result is a string - if (result && typeof (result) !== 'string') { - result = JSON.stringify(result); - } - - window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + result); - } -} - -window.HybridWebView.Init(); diff --git a/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj b/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj index 84cd5c8d3f47..168f84f8b420 100644 --- a/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj +++ b/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/scripts/HybridWebView.js b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/scripts/HybridWebView.js deleted file mode 100644 index 134d7d062c3d..000000000000 --- a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/scripts/HybridWebView.js +++ /dev/null @@ -1,149 +0,0 @@ -window.HybridWebView = { - "Init": function Init() { - function DispatchHybridWebViewMessage(message) { - const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); - window.dispatchEvent(event); - } - - if (window.chrome && window.chrome.webview) { - // Windows WebView2 - window.chrome.webview.addEventListener('message', arg => { - DispatchHybridWebViewMessage(arg.data); - }); - } - else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { - // iOS and MacCatalyst WKWebView - window.external = { - "receiveMessage": message => { - DispatchHybridWebViewMessage(message); - } - }; - } - else { - // Android WebView - window.addEventListener('message', arg => { - DispatchHybridWebViewMessage(arg.data); - }); - } - }, - - "SendRawMessage": function SendRawMessage(message) { - window.HybridWebView.__SendMessageInternal('__RawMessage', message); - }, - - "InvokeDotNet": async function InvokeDotNetAsync(methodName, paramValues) { - const body = { - MethodName: methodName - }; - - if (typeof paramValues !== 'undefined') { - if (!Array.isArray(paramValues)) { - paramValues = [paramValues]; - } - - for (var i = 0; i < paramValues.length; i++) { - paramValues[i] = JSON.stringify(paramValues[i]); - } - - if (paramValues.length > 0) { - body.ParamValues = paramValues; - } - } - - const message = JSON.stringify(body); - - var requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; - - const rawResponse = await fetch(requestUrl, { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - const response = await rawResponse.json(); - - if (response) { - if (response.IsJson) { - return JSON.parse(response.Result); - } - - return response.Result; - } - - return null; - }, - - "__SendMessageInternal": function __SendMessageInternal(type, message) { - - const messageToSend = type + '|' + message; - - if (window.chrome && window.chrome.webview) { - // Windows WebView2 - window.chrome.webview.postMessage(messageToSend); - } - else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { - // iOS and MacCatalyst WKWebView - window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend); - } - else { - // Android WebView - hybridWebViewHost.sendMessage(messageToSend); - } - }, - - "__InvokeJavaScript": async function __InvokeJavaScript(taskId, methodName, args) { - try { - var result = null; - if (methodName[Symbol.toStringTag] === 'AsyncFunction') { - result = await methodName(...args); - } else { - result = methodName(...args); - } - window.HybridWebView.__TriggerAsyncCallback(taskId, result); - } catch (ex) { - console.error(ex); - window.HybridWebView.__TriggerAsyncFailedCallback(taskId, ex); - } - }, - - "__TriggerAsyncFailedCallback": function __TriggerAsyncCallback(taskId, error) { - - if (!error) { - json = { - Message: "Unknown error", - StackTrace: Error().stack - }; - } else if (error instanceof Error) { - json = { - Name: error.name, - Message: error.message, - StackTrace: error.stack - }; - } else if (typeof (error) === 'string') { - json = { - Message: error, - StackTrace: Error().stack - }; - } else { - json = { - Message: JSON.stringify(error), - StackTrace: Error().stack - }; - } - - json = JSON.stringify(json); - - window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptFailed', taskId + '|' + json); - }, - - "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) { - // Make sure the result is a string - if (result && typeof (result) !== 'string') { - result = JSON.stringify(result); - } - - window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + result); - } -} - -window.HybridWebView.Init(); diff --git a/src/Core/src/Core.csproj b/src/Core/src/Core.csproj index 964514584c44..fa8aa282a912 100644 --- a/src/Core/src/Core.csproj +++ b/src/Core/src/Core.csproj @@ -69,6 +69,23 @@ + + + ES2020 + false + true + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + <_CopyItems Include="nuget\buildTransitive\**" SubFolder="" Exclude="nuget\**\*.in.*;nuget\**\net-*\*" /> diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.js b/src/Core/src/Handlers/HybridWebView/HybridWebView.js new file mode 100644 index 000000000000..3510bd7144cc --- /dev/null +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.js @@ -0,0 +1,190 @@ +/* + * This file contains the JavaScript code that the HybridWebView control uses to + * communicate between the web view and the .NET host application. + * + * The JavaScript file is generated from TypeScript and should not be modified + * directly. To make changes, modify the TypeScript file and then recompile it. + */ +(() => { + /* + * Initialize the HybridWebView messaging system. + * This method is called once when the page is loaded. + */ + function initHybridWebView() { + function dispatchHybridWebViewMessage(message) { + const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); + window.dispatchEvent(event); + } + if (window.chrome?.webview?.addEventListener) { + // Windows WebView2 + window.chrome.webview.addEventListener('message', (arg) => { + dispatchHybridWebViewMessage(arg.data); + }); + } + else if (window.webkit?.messageHandlers?.webwindowinterop) { + // iOS and MacCatalyst WKWebView + // @ts-ignore - We are extending the global object here + window.external = { + receiveMessage: (message) => { + dispatchHybridWebViewMessage(message); + }, + }; + } + else { + // Android WebView + window.addEventListener('message', (arg) => { + dispatchHybridWebViewMessage(arg.data); + }); + } + } + /* + * Send a message to the .NET host application. + * The message is sent as a string with the following format: `|`. + */ + function sendMessageToDotNet(type, message) { + const messageToSend = type + '|' + message; + if (window.chrome && window.chrome.webview) { + // Windows WebView2 + window.chrome.webview.postMessage(messageToSend); + } + else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { + // iOS and MacCatalyst WKWebView + window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend); + } + else { + // Android WebView + hybridWebViewHost.sendMessage(messageToSend); + } + } + /* + * Send a message to the .NET host application indicating that a JavaScript method invocation failed. + * The error message is sent as a string with the following format: `|`. + */ + function invokeJavaScriptFailedInDotNet(taskId, error) { + let errorObj; + if (!error) { + errorObj = { + Message: "Unknown error", + StackTrace: Error().stack + }; + } + else if (error instanceof Error) { + errorObj = { + Name: error.name, + Message: error.message, + StackTrace: error.stack + }; + } + else if (typeof error === 'string') { + errorObj = { + Message: error, + StackTrace: Error().stack + }; + } + else { + errorObj = { + Message: JSON.stringify(error), + StackTrace: Error().stack + }; + } + let json = JSON.stringify(errorObj); + sendMessageToDotNet('__InvokeJavaScriptFailed', taskId + '|' + json); + } + /* + * Send a message to the .NET host application indicating that a JavaScript method invocation completed. + * The result is sent as a string with the following format: `|`. + */ + function invokeJavaScriptCallbackInDotNet(taskId, result) { + // Make sure the result is a string + if (result && typeof result !== 'string') { + result = JSON.stringify(result); + } + sendMessageToDotNet('__InvokeJavaScriptCompleted', taskId + '|' + result); + } + const HybridWebView = { + /* + * Send a raw message to the .NET host application. + * The message is sent directly and not processed or serialized. + * + * @param message The message to send to the .NET host application. + */ + SendRawMessage: function (message) { + sendMessageToDotNet('__RawMessage', message); + }, + /* + * Invoke a .NET method on the InvokeJavaScriptTarget instance. + * The method name and parameters are serialized and sent to the .NET host application. + * + * @param methodName The name of the .NET method to invoke. + * @param paramValues The parameters to pass to the .NET method. If the method takes no parameters, this can be omitted. + * + * @returns A promise that resolves with the result of the .NET method invocation. + */ + InvokeDotNet: async function (methodName, paramValues) { + const body = { + MethodName: methodName + }; + // if parameters were provided, serialize them first + if (paramValues !== undefined) { + if (!Array.isArray(paramValues)) { + paramValues = [paramValues]; + } + for (let i = 0; i < paramValues.length; i++) { + paramValues[i] = JSON.stringify(paramValues[i]); + } + if (paramValues.length > 0) { + body.ParamValues = paramValues; + } + } + const message = JSON.stringify(body); + const requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; + const rawResponse = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + const response = await rawResponse.json(); + if (!response) { + return null; + } + if (response.IsJson) { + return JSON.parse(response.Result); + } + return response.Result; + }, + /* + * Invoke a JavaScript method from the .NET host application. + * This method is called from the HybridWebViewHandler and is not intended to be used by user applications. + * + * @param taskId The task ID that was provided by the .NET host application. + * @param methodName The JavaScript method to invoke in the global scope. + * @param args The arguments to pass to the JavaScript method. + * + * @returns A promise. + */ + __InvokeJavaScript: async function (taskId, methodName, args) { + try { + let result = null; + // @ts-ignore - We are checking the type of the function here + if (methodName[Symbol.toStringTag] === 'AsyncFunction') { + result = await methodName(...args); + } + else { + result = methodName(...args); + } + invokeJavaScriptCallbackInDotNet(taskId, result); + } + catch (ex) { + console.error(ex); + invokeJavaScriptFailedInDotNet(taskId, ex); + } + } + }; + // Make the following APIs available in global scope for invocation from JS + // @ts-ignore - We are extending the global object here + window['HybridWebView'] = HybridWebView; + // Initialize the HybridWebView + initHybridWebView(); +})(); +export {}; diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts new file mode 100644 index 000000000000..aae6bdc9d7e8 --- /dev/null +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts @@ -0,0 +1,254 @@ +/* + * This file contains the JavaScript code that the HybridWebView control uses to + * communicate between the web view and the .NET host application. + * + * The JavaScript file is generated from TypeScript and should not be modified + * directly. To make changes, modify the TypeScript file and then recompile it. + */ + +export { }; + +// extend the global objects to include the mobile-specific things +declare global { + + // declare the global object that we have added on Android + const hybridWebViewHost: { + sendMessage: (message: string) => void; + }; + + interface External { + receiveMessage: (message: any) => void; + } + + interface Window { + chrome?: { + webview?: { + addEventListener: (event: string, handler: (arg: any) => void) => void; + postMessage: (message: any) => void; + }; + }; + webkit?: { + messageHandlers?: { + webwindowinterop?: { + postMessage: (message: any) => void; + }; + }; + }; + } +} + +interface JSInvokeMethodData { + MethodName: string; + ParamValues?: string[]; +} + +interface JSInvokeError { + Name?: string; + Message: string; + StackTrace?: string; +} + +interface DotNetInvokeResult { + IsJson: boolean; + Result: any; +} + +(() => { + + /* + * Initialize the HybridWebView messaging system. + * This method is called once when the page is loaded. + */ + function initHybridWebView() { + function dispatchHybridWebViewMessage(message: any) { + const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); + window.dispatchEvent(event); + } + + if (window.chrome?.webview?.addEventListener) { + // Windows WebView2 + window.chrome.webview.addEventListener('message', (arg: any) => { + dispatchHybridWebViewMessage(arg.data); + }); + } else if (window.webkit?.messageHandlers?.webwindowinterop) { + // iOS and MacCatalyst WKWebView + // @ts-ignore - We are extending the global object here + window.external = { + receiveMessage: (message: any) => { + dispatchHybridWebViewMessage(message); + }, + }; + } else { + // Android WebView + window.addEventListener('message', (arg: any) => { + dispatchHybridWebViewMessage(arg.data); + }); + } + } + + /* + * Send a message to the .NET host application. + * The message is sent as a string with the following format: `|`. + */ + function sendMessageToDotNet(type: string, message: string) { + const messageToSend = type + '|' + message; + + if (window.chrome && window.chrome.webview) { + // Windows WebView2 + window.chrome.webview.postMessage(messageToSend); + } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { + // iOS and MacCatalyst WKWebView + window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend); + } else { + // Android WebView + hybridWebViewHost.sendMessage(messageToSend); + } + } + + /* + * Send a message to the .NET host application indicating that a JavaScript method invocation failed. + * The error message is sent as a string with the following format: `|`. + */ + function invokeJavaScriptFailedInDotNet(taskId: string, error: any) { + let errorObj: JSInvokeError; + + if (!error) { + errorObj = { + Message: "Unknown error", + StackTrace: Error().stack + }; + } else if (error instanceof Error) { + errorObj = { + Name: error.name, + Message: error.message, + StackTrace: error.stack + }; + } else if (typeof error === 'string') { + errorObj = { + Message: error, + StackTrace: Error().stack + }; + } else { + errorObj = { + Message: JSON.stringify(error), + StackTrace: Error().stack + }; + } + + let json = JSON.stringify(errorObj); + + sendMessageToDotNet('__InvokeJavaScriptFailed', taskId + '|' + json); + } + + /* + * Send a message to the .NET host application indicating that a JavaScript method invocation completed. + * The result is sent as a string with the following format: `|`. + */ + function invokeJavaScriptCallbackInDotNet(taskId: string, result?: any) { + // Make sure the result is a string + if (result && typeof result !== 'string') { + result = JSON.stringify(result); + } + + sendMessageToDotNet('__InvokeJavaScriptCompleted', taskId + '|' + result); + } + + const HybridWebView = { + + /* + * Send a raw message to the .NET host application. + * The message is sent directly and not processed or serialized. + * + * @param message The message to send to the .NET host application. + */ + SendRawMessage: function (message: string) { + sendMessageToDotNet('__RawMessage', message); + }, + + /* + * Invoke a .NET method on the InvokeJavaScriptTarget instance. + * The method name and parameters are serialized and sent to the .NET host application. + * + * @param methodName The name of the .NET method to invoke. + * @param paramValues The parameters to pass to the .NET method. If the method takes no parameters, this can be omitted. + * + * @returns A promise that resolves with the result of the .NET method invocation. + */ + InvokeDotNet: async function (methodName: string, paramValues?: any) { + const body: JSInvokeMethodData = { + MethodName: methodName + }; + + // if parameters were provided, serialize them first + if (paramValues !== undefined) { + if (!Array.isArray(paramValues)) { + paramValues = [paramValues]; + } + + for (let i = 0; i < paramValues.length; i++) { + paramValues[i] = JSON.stringify(paramValues[i]); + } + + if (paramValues.length > 0) { + body.ParamValues = paramValues; + } + } + + const message = JSON.stringify(body); + + const requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; + + const rawResponse = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + const response: DotNetInvokeResult = await rawResponse.json(); + + if (!response) { + return null; + } + + if (response.IsJson) { + return JSON.parse(response.Result); + } + + return response.Result; + }, + + /* + * Invoke a JavaScript method from the .NET host application. + * This method is called from the HybridWebViewHandler and is not intended to be used by user applications. + * + * @param taskId The task ID that was provided by the .NET host application. + * @param methodName The JavaScript method to invoke in the global scope. + * @param args The arguments to pass to the JavaScript method. + * + * @returns A promise. + */ + __InvokeJavaScript: async function (taskId: string, methodName: Function, args: any[]) { + try { + let result: any = null; + // @ts-ignore - We are checking the type of the function here + if (methodName[Symbol.toStringTag] === 'AsyncFunction') { + result = await methodName(...args); + } else { + result = methodName(...args); + } + invokeJavaScriptCallbackInDotNet(taskId, result); + } catch (ex) { + console.error(ex); + invokeJavaScriptFailedInDotNet(taskId, ex); + } + } + }; + + // Make the following APIs available in global scope for invocation from JS + // @ts-ignore - We are extending the global object here + window['HybridWebView'] = HybridWebView; + + // Initialize the HybridWebView + initHybridWebView(); + +})();