Поделиться через


HybridWebView

Просмотрите пример. Обзор примера

Пользовательский интерфейс многоплатформенного приложения .NET (.NET MAUI) HybridWebView позволяет размещать произвольное содержимое HTML/JS/CSS в веб-представлении и обеспечивает обмен данными между кодом в веб-представлении (JavaScript) и кодом, на котором размещено веб-представление (C#/.NET). Например, если у вас есть существующее приложение React JS, его можно разместить в кроссплатформенной машинном приложении .NET MAUI и создать серверную часть приложения с помощью C# и .NET.

HybridWebView определяет следующие свойства:

  • DefaultFileТип string?, который указывает файл внутри HybridRoot файла, который должен служить в качестве файла по умолчанию. Значение по умолчанию — index.html.
  • HybridRootstring?Тип , который представляет собой путь в необработанных ресурсах приложения, содержащих содержимое веб-приложения. Значение по умолчанию — wwwroot, которое сопоставляется с Resources/Raw/wwwroot.

Кроме того, HybridWebView определяет событие, которое возникает RawMessageReceived при получении необработанного сообщения. Объект HybridWebViewRawMessageReceivedEventArgs , который сопровождает событие, Message определяет свойство, содержащее сообщение.

Код C# приложения может вызывать синхронные и асинхронные методы JavaScript в HybridWebView рамках и InvokeJavaScriptAsync EvaluateJavaScriptAsync методов. Код JavaScript приложения также может синхронно вызывать методы C#. Дополнительные сведения см. в разделе "Вызов JavaScript" из C# и вызов C# из JavaScript.

Чтобы создать приложение .NET MAUI, HybridWebView необходимо:

  • Веб-содержимое приложения, состоящее из статического HTML, JavaScript, CSS, изображений и других файлов.
  • Элемент HybridWebView управления в составе пользовательского интерфейса приложения. Это можно сделать, ссылаясь на него в XAML приложения.
  • Код в веб-содержимом и в C#/.NET, который использует HybridWebView API для отправки сообщений между двумя компонентами.

Все приложение, включая веб-содержимое, упаковано и выполняется локально на устройстве и может быть опубликовано в применимых магазинах приложений. Веб-содержимое размещается в собственном элементе управления веб-представления и выполняется в контексте приложения. Любая часть приложения может получить доступ к внешним веб-службам, но не требуется.

Внимание

По умолчанию элемент HybridWebView управления не будет доступен, если включена полная обрезка или собственный AOT. Сведения об изменении этого поведения см. в разделе "Параметры функции обрезки".

Создание приложения .NET MAUI HybridWebView

Создание приложения .NET MAUI с помощью HybridWebView:

  1. Откройте существующий проект приложения .NET MAUI или создайте проект приложения .NET MAUI.

  2. Добавьте веб-содержимое в проект приложения .NET MAUI.

    Веб-содержимое приложения должно быть включено в проект .NET MAUI в качестве необработанных ресурсов. Необработанный ресурс — это любой файл в папке Resources\Raw приложения и включает вложенные папки. По умолчанию HybridWebViewвеб-содержимое должно быть помещено в папку Resources\Raw\wwwroot с основным файлом с именем index.html.

    Простое приложение может содержать следующие файлы и содержимое:

    • Resources\Raw\wwwroot\index.html с содержимым для основного пользовательского интерфейса:

      <!DOCTYPE html>
      
      <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
      <head>
          <meta charset="utf-8" />
          <title></title>
          <link rel="icon" href="data:,">
          <link rel="stylesheet" href="styles/app.css">
          <script src="scripts/HybridWebView.js"></script>
          <script>
              function LogMessage(msg) {
                  var messageLog = document.getElementById("messageLog");
                  messageLog.value += '\r\n' + msg;
              }
      
              window.addEventListener(
                  "HybridWebViewMessageReceived",
                  function (e) {
                      LogMessage("Raw message: " + e.detail.message);
                  });
      
              function AddNumbers(a, b) {
                  var result = {
                      "result": a + b,
                      "operationName": "Addition"
                  };
                  return result;
              }
      
              var count = 0;
      
              async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
                  const response = await fetch("/asyncdata.txt");
                  if (!response.ok) {
                      throw new Error(`HTTP error: ${response.status}`);
                  }
                  var jsonData = await response.json();
      
                  jsonData[s1] = s2;
      
                  const msg = 'JSON data is available: ' + JSON.stringify(jsonData);
                  window.HybridWebView.SendRawMessage(msg)
      
                  return jsonData;
              }
      
              async function InvokeDoSyncWork() {
                  LogMessage("Invoking DoSyncWork");
                  await window.HybridWebView.InvokeDotNet('DoSyncWork');
                  LogMessage("Invoked DoSyncWork");
              }
      
              async function InvokeDoSyncWorkParams() {
                  LogMessage("Invoking DoSyncWorkParams");
                  await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']);
                  LogMessage("Invoked DoSyncWorkParams");
              }
      
              async function InvokeDoSyncWorkReturn() {
                  LogMessage("Invoking DoSyncWorkReturn");
                  const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn');
                  LogMessage("Invoked DoSyncWorkReturn, return value: " + retValue);
              }
      
              async function InvokeDoSyncWorkParamsReturn() {
                  LogMessage("Invoking DoSyncWorkParamsReturn");
                  const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']);
                  LogMessage("Invoked DoSyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value);
              }
      
          </script>
      </head>
      <body>
          <div>
              Hybrid sample!
          </div>
          <div>
              <button onclick="window.HybridWebView.SendRawMessage('Message from JS! ' + (count++))">Send message to C#</button>
          </div>
          <div>
              <button onclick="InvokeDoSyncWork()">Call C# sync method (no params)</button>
              <button onclick="InvokeDoSyncWorkParams()">Call C# sync method (params)</button>
              <button onclick="InvokeDoSyncWorkReturn()">Call C# method (no params) and get simple return value</button>
              <button onclick="InvokeDoSyncWorkParamsReturn()">Call C# method (params) and get complex return value</button>
          </div>
          <div>
              Log: <textarea readonly id="messageLog" style="width: 80%; height: 10em;"></textarea>
          </div>
          <div>
              Consider checking out this PDF: <a href="docs/sample.pdf">sample.pdf</a>
          </div>
      </body>
      </html>
      
    • Resources\Raw\wwwroot\scripts\HybridWebView.js со стандартной HybridWebView библиотекой JavaScript:

      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": function __InvokeJavaScript(taskId, methodName, args) {
              if (methodName[Symbol.toStringTag] === 'AsyncFunction') {
                  // For async methods, we need to call the method and then trigger the callback when it's done
                  const asyncPromise = methodName(...args);
                  asyncPromise
                      .then(asyncResult => {
                          window.HybridWebView.__TriggerAsyncCallback(taskId, asyncResult);
                      })
                      .catch(error => console.error(error));
              } else {
                  // For sync methods, we can call the method and trigger the callback immediately
                  const syncResult = methodName(...args);
                  window.HybridWebView.__TriggerAsyncCallback(taskId, syncResult);
              }
          },
      
          "__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();
      

    Затем добавьте в проект любое дополнительное веб-содержимое.

    Предупреждение

    В некоторых случаях Visual Studio может добавлять записи в CSPROJ-файл проекта, который является неверным. При использовании расположения по умолчанию для необработанных ресурсов не должно быть никаких записей для этих файлов или папок в CSPROJ-файле .

  3. Добавьте элемент управления в HybridWebView приложение:

    <Grid RowDefinitions="Auto,*"
          ColumnDefinitions="*">
        <Button Text="Send message to JavaScript"
                Clicked="OnSendMessageButtonClicked" />
        <HybridWebView x:Name="hybridWebView"
                       RawMessageReceived="OnHybridWebViewRawMessageReceived"
                       Grid.Row="1" />
    </Grid>
    
  4. Измените CreateMauiApp метод класса MauiProgram , чтобы включить средства разработчика в базовых элементах управления WebView при выполнении приложения в конфигурации отладки. Для этого вызовите AddHybridWebViewDeveloperTools метод объекта IServiceCollection :

    using Microsoft.Extensions.Logging;
    
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });
    
    #if DEBUG
            builder.Services.AddHybridWebViewDeveloperTools();
            builder.Logging.AddDebug();            
    #endif
            // Register any app services on the IServiceCollection object
    
            return builder.Build();
        }
    }
    
  5. HybridWebView Используйте API для отправки сообщений между кодом JavaScript и C#:

    private void OnSendMessageButtonClicked(object sender, EventArgs e)
    {
        hybridWebView.SendRawMessage($"Hello from C#!");
    }
    
    private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
    {
        await DisplayAlert("Raw Message Received", e.Message, "OK");
    }
    

    Приведенные выше сообщения классируются как необработанные, так как дополнительная обработка не выполняется. Вы также можете закодировать данные в сообщении для более расширенного обмена сообщениями.

Вызов JavaScript из C#

Код C# приложения может синхронно и асинхронно вызывать методы JavaScript в пределах приложения HybridWebViewс необязательными параметрами и необязательным возвращаемым значением. Это можно сделать с помощью InvokeJavaScriptAsync методов и EvaluateJavaScriptAsync методов.

  • Метод EvaluateJavaScriptAsync запускает код JavaScript, предоставленный с помощью параметра, и возвращает результат в виде строки.
  • Метод InvokeJavaScriptAsync вызывает указанный метод JavaScript, при необходимости передавая значения параметров, и задает универсальный аргумент, указывающий тип возвращаемого значения. Он возвращает объект универсального типа аргумента, который содержит возвращаемое значение вызываемого метода JavaScript. Внутренние параметры и возвращаемые значения кодируются в формате JSON.

Вызов синхронного JavaScript

Синхронные методы JavaScript можно вызывать с EvaluateJavaScriptAsync помощью методов и InvokeJavaScriptAsync методов. В следующем примере InvokeJavaScriptAsync метод используется для демонстрации вызова JavaScript, внедренного в веб-содержимое приложения. Например, простой метод Javascript для добавления двух чисел можно определить в веб-содержимом:

function AddNumbers(a, b) {
    return a + b;
}

Метод AddNumbers JavaScript можно вызвать из C# с InvokeJavaScriptAsync помощью метода:

double x = 123d;
double y = 321d;

double result = await hybridWebView.InvokeJavaScriptAsync<double>(
    "AddNumbers", // JavaScript method name
    HybridSampleJSContext.Default.Double, // JSON serialization info for return type
    [x, y], // Parameter values
    [HybridSampleJSContext.Default.Double, HybridSampleJSContext.Default.Double]); // JSON serialization info for each parameter

Вызов метода требует указания JsonTypeInfo объектов, включающих сведения о сериализации для типов, используемых в операции. Эти объекты создаются автоматически, включая следующий partial класс в проекте:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(double))]
internal partial class HybridSampleJsContext : JsonSerializerContext
{
    // This type's attributes specify JSON serialization info to preserve type structure
    // for trimmed builds.
}

Внимание

Класс HybridSampleJsContext должен быть partial таким, чтобы создание кода может обеспечить реализацию при компиляции проекта. Если тип вложен в другой тип, этот тип также должен быть partial.

Вызов асинхронного JavaScript

Асинхронные методы JavaScript можно вызывать с помощью EvaluateJavaScriptAsync методов и InvokeJavaScriptAsync методов. В следующем примере InvokeJavaScriptAsync метод используется для демонстрации вызова JavaScript, внедренного в веб-содержимое приложения. Например, метод Javascript, который асинхронно извлекает данные, можно определить в веб-содержимом:

async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
    const response = await fetch("/asyncdata.txt");
    if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
    }
    var jsonData = await response.json();
    jsonData[s1] = s2;

    return jsonData;
}

Метод EvaluateMeWithParamsAndAsyncReturn JavaScript можно вызвать из C# с InvokeJavaScriptAsync помощью метода:

Dictionary<string, string> asyncResult = await hybridWebView.InvokeJavaScriptAsync<Dictionary<string, string>>(
    "EvaluateMeWithParamsAndAsyncReturn", // JavaScript method name
    HybridSampleJSContext.Default.DictionaryStringString, // JSON serialization info for return type
    ["new_key", "new_value"], // Parameter values
    [HybridSampleJSContext.Default.String, HybridSampleJSContext.Default.String]); // JSON serialization info for each parameter

В этом примере asyncResult содержит Dictionary<string, string> данные JSON из веб-запроса.

Вызов метода требует указания JsonTypeInfo объектов, включающих сведения о сериализации для типов, используемых в операции. Эти объекты создаются автоматически, включая следующий partial класс в проекте:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(string))]
internal partial class HybridSampleJSContext : JsonSerializerContext
{
    // This type's attributes specify JSON serialization info to preserve type structure
    // for trimmed builds.  
}

Внимание

Класс HybridSampleJsContext должен быть partial таким, чтобы создание кода может обеспечить реализацию при компиляции проекта. Если тип вложен в другой тип, этот тип также должен быть partial.

Вызов C# из JavaScript

Код JavaScript приложения в HybridWebView рамках метода C# может синхронно вызывать методы C# с необязательными параметрами и необязательным возвращаемым значением. Это выполняется посредством следующих настроек.

  • Определение общедоступных методов C#, которые будут вызываться из JavaScript.
  • SetInvokeJavaScriptTarget Вызов метода для задания объекта, который будет целевым объектом вызовов JavaScript из .HybridWebView
  • Вызов методов C# из JavaScript.

Внимание

Асинхронное вызов методов C# из JavaScript в настоящее время не поддерживается.

В следующем примере определяются четыре открытых метода для вызова из JavaScript:

public partial class MainPage : ContentPage
{
    ...  

    public void DoSyncWork()
    {
        Debug.WriteLine("DoSyncWork");
    }

    public void DoSyncWorkParams(int i, string s)
    {
        Debug.WriteLine($"DoSyncWorkParams: {i}, {s}");
    }

    public string DoSyncWorkReturn()
    {
        Debug.WriteLine("DoSyncWorkReturn");
        return "Hello from C#!";
    }

    public SyncReturn DoSyncWorkParamsReturn(int i, string s)
    {
        Debug.WriteLine($"DoSyncWorkParamReturn: {i}, {s}");
        return new SyncReturn
        {
            Message = "Hello from C#!" + s,
            Value = i
        };
    }

    public class SyncReturn
    {
        public string? Message { get; set; }
        public int Value { get; set; }
    }  
}

Затем необходимо вызвать SetInvokeJavaScriptTarget метод, чтобы задать объект, который будет целевым объектом вызовов JavaScript из HybridWebView:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        hybridWebView.SetInvokeJavaScriptTarget(this);
    }

    ...
}

Открытые методы набора объектов с помощью SetInvokeJavaScriptTarget метода можно вызвать из JavaScript с window.HybridWebView.InvokeDotNet помощью функции:

await window.HybridWebView.InvokeDotNet('DoSyncWork');
await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']);
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn');
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']);

Функция window.HybridWebView.InvokeDotNet JavaScript вызывает указанный метод C# с необязательными параметрами и необязательным возвращаемым значением.

Примечание.

window.HybridWebView.InvokeDotNet Вызов функции JavaScript требует, чтобы приложение включало библиотеку JavaScript HybridWebView.js, указанную ранее в этой статье.