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..f46f0840aa7c 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 52d67172af49..000000000000 --- a/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js +++ /dev/null @@ -1,145 +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) { - const json = JSON.stringify(result); - window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + json); - } -} - -window.HybridWebView.Init(); diff --git a/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj b/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj index 84cd5c8d3f47..776daee3c3ba 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 52d67172af49..000000000000 --- a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/scripts/HybridWebView.js +++ /dev/null @@ -1,145 +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) { - const json = JSON.stringify(result); - window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + json); - } -} - -window.HybridWebView.Init(); diff --git a/src/Core/src/Core.csproj b/src/Core/src/Core.csproj index 964514584c44..71304968edb8 100644 --- a/src/Core/src/Core.csproj +++ b/src/Core/src/Core.csproj @@ -69,6 +69,24 @@ + + + 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..4c6a63f0274d --- /dev/null +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.js @@ -0,0 +1,189 @@ +/* + * 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 if (window.hybridWebViewHost) { + // Android WebView + window.hybridWebViewHost.sendMessage(messageToSend); + } + else { + console.error('Unable to send messages to .NET because the host environment for the HybridWebView was unknown.'); + } + } + /* + * 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 + }; + } + const 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) { + const json = JSON.stringify(result); + sendMessageToDotNet('__InvokeJavaScriptCompleted', taskId + '|' + json); + } + 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(); +})(); diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts new file mode 100644 index 000000000000..2657dec19b3d --- /dev/null +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts @@ -0,0 +1,252 @@ +/* + * 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. + */ + +/* + * Extend the global objects to include the interfaces that are used by the + * HybridWebView and some operating systems and/or browser hosts. + */ +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; + }; + }; + }; + + // Declare the global object that we have added on Android. + hybridWebViewHost?: { + sendMessage: (message: string) => void; + }; +} + +/* + * The following interfaces define the shape of the messages that are sent between + * the web view and the .NET host application. + */ +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 if (window.hybridWebViewHost) { + // Android WebView + window.hybridWebViewHost.sendMessage(messageToSend); + } else { + console.error('Unable to send messages to .NET because the host environment for the HybridWebView was unknown.'); + } + } + + /* + * 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 + }; + } + + const 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) { + const json = JSON.stringify(result); + + sendMessageToDotNet('__InvokeJavaScriptCompleted', taskId + '|' + json); + } + + 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(); + +})();