HybridWebView у .NET MAUI 10: практичний посібник з JavaScript ↔ C# інтеграції (2026)

HybridWebView у .NET MAUI 10 хостить локальний веб-контент і встановлює типізований двосторонній міст між JavaScript та C#. Розберемо InvokeJavaScriptAsync, InvokeMethodAsync, події ініціалізації, обробку JS-винятків та підводні камені AOT з прикладами коду.

HybridWebView у .NET MAUI 10: гайд (2026)

Оновлено: 7 серпня 2026 р.

HybridWebView у .NET MAUI 10 — це елемент керування, що дає змогу хостити локальний HTML/JS/CSS-контент усередині нативного мобільного додатка та встановлювати двосторонню взаємодію між JavaScript у веб-в'ю й C#-кодом хоста через типізовані виклики методів і сирі повідомлення. На відміну від BlazorWebView, він не тягне за собою Blazor-стек, працює з будь-яким SPA (React, Vue, ванільним JS), а в .NET 10 отримав нативну обробку JS-винятків і події ініціалізації.

  • HybridWebView хостить локальний HTML/JS/CSS з папки Resources/Raw/wwwroot без залежностей від Blazor або віддалених серверів.
  • Двостороння комунікація йде через InvokeJavaScriptAsync (C# у JS) та window.HybridWebView.InvokeMethodAsync (JS у C#) з JSON-серіалізацією через source generators.
  • У .NET 10 з'явився перемикач HybridWebView.InvokeJavaScriptThrowsExceptions, який пробрасує JS-помилки як .NET-винятки замість тихого проковтування.
  • Події WebViewInitializing та WebViewInitialized в .NET 10 дають доступ до нативного WebView2/WKWebView для тонкого налаштування.
  • HybridWebView несумісний з повним трімінгом і Native AOT за замовчуванням, потрібен feature switch у csproj.
  • Порівняно з BlazorWebView, HybridWebView легший, швидше стартує та не серіалізує кожен render-tree через міст, але вимагає, щоб ви писали фронтенд самі.

Що таке HybridWebView в .NET MAUI 10

Отже, почнімо з базової картинки. HybridWebView — це View, який рендерить локальні HTML/CSS/JS-ресурси всередині нативного WebView2 (Windows), WKWebView (iOS/Mac Catalyst) або Android WebView, а зверху додає протокол обміну повідомленнями. Він з'явився ще в .NET MAUI 9 як відповідь на запит спільноти. BlazorWebView занадто важкий, звичайний WebView не має чистого JS-моста, а власноруч написаний JS-bridge через EvaluateJavaScriptAsync було легко зламати рядковим екрануванням.

Я шипив HybridWebView у трьох апах. Один із них, B2B-калькулятор із важкою логікою кошторисів, раніше жив як React-SPA і був переписаний під MAUI без відмови від існуючого фронтенд-коду. Ідея проста: фронтенд команда продовжує писати React, а нативна команда через HybridWebView прокидає доступ до біометрії, файлової системи та фонової синхронізації. На відміну від чистого WebView, тут не треба виносити фронт у CDN. Усе пакується в застосунок і працює офлайн.

Три сценарії, де HybridWebView реально виграє: перенесення існуючого JS/TS SPA у нативну обгортку без переписування, побудова кастомних UI-компонентів на HTML/Canvas (діаграми, редактори), і додатки з часто змінюваним контентом, який зручно шаблонізувати через веб-стек. Якщо у вас чиста бізнес-логіка на C# і немає готового веб-коду, вам, найімовірніше, потрібен нативний MAUI, а не HybridWebView.

Налаштування проєкту та структура папок

Створіть новий .NET MAUI-проєкт із шаблону dotnet new maui на .NET 10 SDK і додайте посилання на Microsoft.Maui.Controls версії 10.0 або новішу. HybridWebView вже включений у базовий пакет, окремих NuGet-залежностей не потрібно. Уся веб-складова живе як сирі ресурси: створіть у корені проєкту папку Resources/Raw/wwwroot, куди покладіть index.html, скрипти, стилі та зображення.

У файлі .csproj сирі ресурси вже включені за замовчуванням через шаблон <MauiAsset Include="Resources\Raw\**">. Якщо ви хочете тримати кілька окремих веб-модулів (наприклад, головний UI та вбудований редактор), створюйте підпапки на кшталт Resources/Raw/editor, і потім у XAML вказуйте HybridRoot="editor". Властивість DefaultFile за замовчуванням дорівнює index.html, але її можна перевизначити для роутингу до конкретного файлу.

Перший HybridWebView: index.html та XAML

Мінімальний робочий приклад складається з трьох файлів: сторінка XAML, HTML-контент і JS-міст. Ось базова розмітка сторінки. Зверніть увагу, що на Windows потрібно явно вказати x:Name, бо ми звертатимемось до контролу з code-behind:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="HybridDemo.MainPage">
    <Grid RowDefinitions="Auto,*">
        <Button Grid.Row="0"
                Text="Викликати JS"
                Clicked="OnCallJsClicked" />
        <HybridWebView Grid.Row="1"
                       x:Name="hybridView"
                       HybridRoot="wwwroot"
                       DefaultFile="index.html"
                       RawMessageReceived="OnRawMessageReceived" />
    </Grid>
</ContentPage>

А ось відповідний index.html, який реєструє глобальні функції для виклику з C# і надсилає власне вітання назад:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8" />
    <title>HybridWebView Demo</title>
</head>
<body>
    <h1 id="greeting">Готово до інтеграції</h1>
    <script>
        // Глобальна функція, яку викличе C# через InvokeJavaScriptAsync
        window.setGreeting = function (name, timestamp) {
            const el = document.getElementById("greeting");
            el.innerText = `Привіт, ${name}! Час: ${timestamp}`;
            return { ok: true, length: name.length };
        };

        // Надсилаємо сире повідомлення в C# при завантаженні сторінки
        window.addEventListener("load", () => {
            window.HybridWebView.SendRawMessage("page-loaded");
        });
    </script>
</body>
</html>

Оскільки ми будуємо весь проєкт від нуля, ознайомтеся також із гайдом із локальних баз даних у MAUI, якщо плануєте зберігати результати обміну між JS та C# у SQLite.

Як викликати JavaScript з C#

Основний метод для типізованого виклику JS-функцій, це InvokeJavaScriptAsync<TReturn>. Він серіалізує параметри у JSON, викликає глобальну функцію з window у контексті сторінки та десеріалізує повернуте значення. Оскільки .NET 8+ вимагає source-generated JSON для трімінгу, вам знадобиться клас, що успадковує JsonSerializerContext:

using System.Text.Json.Serialization;

public record GreetingResult(bool Ok, int Length);

[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(DateTime))]
[JsonSerializable(typeof(GreetingResult))]
internal partial class HybridJsonContext : JsonSerializerContext { }

А в code-behind сторінки виклик виглядає так. Зверніть увагу, що масиви paramValues та paramJsonTypeInfos мають ту саму довжину, а індекси відповідають один одному:

private async void OnCallJsClicked(object sender, EventArgs e)
{
    try
    {
        var result = await hybridView.InvokeJavaScriptAsync<GreetingResult>(
            methodName: "setGreeting",
            returnTypeJsonTypeInfo: HybridJsonContext.Default.GreetingResult,
            paramValues: new object[] { "Марку", DateTime.Now },
            paramJsonTypeInfos: new JsonTypeInfo[]
            {
                HybridJsonContext.Default.String,
                HybridJsonContext.Default.DateTime
            });

        await DisplayAlert(
            "JS повернув",
            $"Ok={result?.Ok}, Length={result?.Length}",
            "OK");
    }
    catch (HybridWebViewInvokeJavaScriptException ex)
    {
        // спрацьовує лише якщо ввімкнено feature switch (див. розділ .NET 10)
        await DisplayAlert("JS помилка", ex.Message, "OK");
    }
}

Практична порада: якщо треба лише виконати JS без параметрів і без повернення (наприклад, скинути стан UI), користуйтесь EvaluateJavaScriptAsync. Він швидший, бо не проходить через JSON-міст. А InvokeJavaScriptAsync резервуйте для типізованих викликів з даними.

Як викликати C# з JavaScript

Зворотний напрямок теж підтримується через типізований API. У JavaScript ви викликаєте window.HybridWebView.InvokeMethodAsync, передаючи ім'я .NET-методу, а на C#-стороні реєструєте методи через SetInvokeJavaScriptTarget. У .NET 10 API став чистішим: об'єкт-таргет має бути звичайним POCO з відкритими методами, а серіалізація параметрів автоматична, якщо ви передаєте JsonSerializerOptions з увімкненими source generators.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        hybridView.SetInvokeJavaScriptTarget(new NativeBridge());
    }
}

public class NativeBridge
{
    // JavaScript викликає: await window.HybridWebView.InvokeMethodAsync("Add", 2, 3)
    public int Add(int a, int b) => a + b;

    // Асинхронний виклик з фоновою роботою
    public async Task<string> FetchUserAsync(int id)
    {
        await Task.Delay(200); // імітація I/O
        return $"user-{id}@company.local";
    }
}

А ось як це виглядає з боку JavaScript. InvokeMethodAsync завжди повертає Promise, навіть якщо .NET-метод синхронний:

async function callNative() {
    const sum = await window.HybridWebView.InvokeMethodAsync("Add", 2, 3);
    console.log("sum =", sum); // 5

    const email = await window.HybridWebView.InvokeMethodAsync(
        "FetchUserAsync", 42);
    console.log("email =", email);
}

Сирі повідомлення та SendRawMessage

Не всі сценарії варто оформлювати як RPC-виклики. Іноді треба просто передати рядок (телеметрію, лог, подію життєвого циклу) і не морочитись із JSON-контекстами. Для цього існує SendRawMessage в обох напрямках і подія RawMessageReceived на C#-стороні.

// C# -> JS
hybridView.SendRawMessage("theme:dark");

// JS отримує через:
window.HybridWebView.addEventListener("HybridWebViewMessage", (e) => {
    console.log("Отримано з C#:", e.detail.message);
});

// JS -> C#
window.HybridWebView.SendRawMessage("analytics:view:home");

// C# отримує:
private void OnRawMessageReceived(
    object sender,
    HybridWebViewRawMessageReceivedEventArgs e)
{
    logger.LogInformation("Веб-подія: {Message}", e.Message);
}

Це особливо зручно для fire-and-forget-телеметрії, коли ви не чекаєте відповіді. У додатку-калькуляторі я перекинув на цей канал усі breadcrumb-події для Sentry: веб-сторінка викидає рядки виду bc:calc:step-3, а C# складає їх у чергу та відправляє батчами разом із нативними подіями. Це швидше за RPC і не тратить бюджет source generators.

Для порівняння з іншими підходами до архітектури клієнтського коду в MAUI подивіться розбір CommunityToolkit.Mvvm з джерельними генераторами. Там я показую, як через [ObservableProperty] будувати ViewModel, який можна дзеркалити у JS-стан через сирі повідомлення.

Новинки .NET 10: винятки та події ініціалізації

Чесно кажучи, це моя улюблена частина релізу. У .NET 10 команда MAUI додала дві довгоочікувані фічі. Перша — це прозоре пробрасування JS-винятків у .NET-код. За замовчуванням, якщо ваш JS кине помилку всередині InvokeJavaScriptAsync, ви отримаєте null замість повернутого значення й жодного знаку про збій. Роками це спричиняло години дебагу. Тепер достатньо додати перемикач у статичному конструкторі MauiProgram:

public static class MauiProgram
{
    static MauiProgram()
    {
        AppContext.SetSwitch(
            "HybridWebView.InvokeJavaScriptThrowsExceptions",
            true);
    }

    public static MauiApp CreateMauiApp() { /* ... */ }
}

Після цього будь-який throw new Error("...") у JS перетвориться на HybridWebViewInvokeJavaScriptException у C#, який можна ловити try/catch і відправляти в централізований обробник помилок. Офіційне описання поведінки є в Microsoft Learn для HybridWebView.

Друга новинка, події WebViewInitializing та WebViewInitialized. Вони дають доступ до нативного об'єкта WebView (наприклад, CoreWebView2 на Windows чи WKWebView на iOS) до й після ініціалізації, що дозволяє налаштовувати заголовки CSP, реєструвати кастомні URL-схеми, включати віддалений дебагінг у Debug-збірках або пробрасувати cookies із нативної HttpClient-сесії. Приклад для Windows:

hybridView.WebViewInitialized += (s, e) =>
{
#if WINDOWS
    var native = e.WebView as
        Microsoft.Web.WebView2.Core.CoreWebView2;
    native?.Settings.IsScriptEnabled = true;
    native?.Settings.AreDevToolsEnabled = false; // прод
#endif
};

HybridWebView проти BlazorWebView

Це найчастіше питання, яке я чую від команд, що обирають між двома підходами. Коротка відповідь: якщо у вас уже є React/Vue/Svelte-фронт і сильна веб-команда, беріть HybridWebView. Якщо ви пишете все з нуля, а команда C#-центрична, беріть Blazor Hybrid. Ось детальне порівняння за важливими критеріями:

ХарактеристикаHybridWebViewBlazorWebView
Розмір runtimeЛегкий (нативний WebView + JS-міст)Важкий (тягне Blazor render-tree)
Час холодного старту~200-400 мс на середньому Android~600-900 мс через ініціалізацію Blazor circuit
Стек фронтендуБудь-який HTML/CSS/JS (React, Vue, ванільний)Тільки Razor Components
Швидкість JS-interopШвидка (прямий JSON-міст)Повільніша (маршалінг render-tree)
Досвід командиПотрібні веб-розробникиДостатньо C#/Razor-розробників
Hot reloadЧерез будь-який Vite/Webpack dev serverВбудований у .NET Hot Reload
DebuggingChrome DevTools через remote inspectVisual Studio C#-дебагер + F12
Ідеальний сценарійПортування SPA, кастомні UI-модуліШвидкий MVP на існуючих Blazor-компонентах

Я користуюсь простим правилом. Якщо ви пишете рівно один UI-фрагмент, що має ділити код із веб-версією застосунку, то HybridWebView випереджає BlazorWebView за всіма показниками, окрім швидкості початкової розробки. У продакшені старт застосунка на 300-500 мс швидший, а це критично для утримання користувача на Android-девайсах низького ешелону.

Виключенням є сценарії, коли у вас уже є Blazor Server або WebAssembly застосунок і ви хочете переупакувати його в нативний shell без переписування. Тоді BlazorWebView очевидний вибір. Для нових проєктів, які починаєте з нуля саме як мобільний додаток, HybridWebView як мінімум заслуговує на benchmark перед прийняттям рішення.

Тримінг, AOT та підводні камені продакшену

За замовчуванням HybridWebView не працює з повним трімінгом і Native AOT. Контроль вирізається лінкером, бо тримається на рефлексії. Це головний тонкий лід, на який наступають команди, що вмикають AOT для оптимізації розміру. Щоб залишити HybridWebView живим, додайте feature switch у .csproj:

<ItemGroup>
    <RuntimeHostConfigurationOption
        Include="Microsoft.Maui.Controls.HybridWebView.IsSupported"
        Value="true"
        Trim="true" />
</ItemGroup>

Після цього залежності контроля не будуть вирізатись, і застосунок продовжить білдитись під AOT. Але майте на увазі: HybridWebView сильно спирається на System.Text.Json, тому вам знадобляться правильні JsonSerializerContext-класи, інакше на runtime отримаєте NotSupportedException. Про AOT-специфіку я детальніше писав у гайді з Native AOT у MAUI 10.

Три інші проблеми, які варто передбачити заздалегідь. По-перше, WebView на Android не отримує оновлень разом із застосунком. Це компонент системи, і на старих версіях Android ви можете зіткнутись із застарілим Chromium. По-друге, iOS обмежує розмір локально захостованого JS через WKWebView memory pressure, тримайте бандл менше 5 МБ, щоб не отримати випадковий crash на iPad із багатьма відкритими вкладками. По-третє, вміст Resources/Raw не має обробки CSP за замовчуванням, тож задавайте Content-Security-Policy через <meta>-тег у HTML і не покладайтесь на нативний рівень.

Для інструментації та моніторингу продуктивності всього MAUI-стеку рекомендую офіційну документацію docs-maui на GitHub та issue #25782 в dotnet/maui, де обговорюються нюанси нових .NET 9/10 API для InvokeJavaScript. Обидва джерела оновлюються синхронно з релізами, на відміну від сторонніх блогів.

Часті запитання

Чим HybridWebView відрізняється від звичайного WebView в .NET MAUI?

Звичайний WebView показує віддалену або локальну сторінку без структурованого JS-моста, вам треба самому серіалізувати параметри та парсити відповіді через рядкове екранування. HybridWebView додає типізований протокол на базі InvokeJavaScriptAsync/InvokeMethodAsync, автоматичну JSON-серіалізацію через source generators та подію RawMessageReceived для сирих повідомлень. Крім того, HybridWebView примусово хостить локальний контент з Resources/Raw, тому не працює з Source="https://...".

Як передавати параметри з JavaScript у C# у HybridWebView?

Використовуйте window.HybridWebView.InvokeMethodAsync("MethodName", param1, param2) у JavaScript, параметри будуть автоматично серіалізовані в JSON. На C#-стороні зареєструйте таргет через hybridView.SetInvokeJavaScriptTarget(new MyBridge()), і публічні методи класу MyBridge стануть доступні для виклику. Складні об'єкти працюють, якщо їхні типи оголошені в JsonSerializerContext.

Чи можна використовувати React або Vue всередині HybridWebView?

Так, HybridWebView не накладає обмежень на фронтенд-стек. Ви пишете React/Vue/Svelte-застосунок як зазвичай, збираєте його через Vite або Webpack у плоский bundle.js + index.html, і копіюєте результат у Resources/Raw/wwwroot. Далі викликайте C# з ваших компонентів через window.HybridWebView.InvokeMethodAsync. Це найпоширеніший сценарій для команд, що переносять існуючий SPA у нативну обгортку.

Чому HybridWebView повертає null замість помилки, коли JS кидає виняток?

Це поведінка за замовчуванням для сумісності зі старими версіями. У .NET 10 увімкніть перемикач AppContext.SetSwitch("HybridWebView.InvokeJavaScriptThrowsExceptions", true) у статичному конструкторі MauiProgram. Після цього JS-помилки будуть пробрасуватись як HybridWebViewInvokeJavaScriptException у .NET-код, і ви зможете їх ловити через try/catch. У попередніх версіях MAUI цей контроль був відсутній.

Чи працює HybridWebView з Native AOT у .NET MAUI 10?

Не за замовчуванням, контроль вирізається лінкером через залежність від рефлексії. Щоб залишити його доступним при AOT-компіляції, додайте RuntimeHostConfigurationOption з ім'ям Microsoft.Maui.Controls.HybridWebView.IsSupported у .csproj, а також переконайтесь, що всі типи, які ви серіалізуєте між JS та C#, оголошені в класі, що успадковує JsonSerializerContext. Без цього ви отримаєте NotSupportedException на runtime.

Marcus Chen
Про Автора Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.