HybridWebView у .NET MAUI 10: практичний посібник з JavaScript ↔ C# інтеграції (2026)
HybridWebView у .NET MAUI 10 хостить локальний веб-контент і встановлює типізований двосторонній міст між JavaScript та C#. Розберемо InvokeJavaScriptAsync, InvokeMethodAsync, події ініціалізації, обробку JS-винятків та підводні камені AOT з прикладами коду.
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:
А ось відповідний 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-метод синхронний:
Не всі сценарії варто оформлювати як RPC-виклики. Іноді треба просто передати рядок (телеметрію, лог, подію життєвого циклу) і не морочитись із JSON-контекстами. Для цього існує SendRawMessage в обох напрямках і подія RawMessageReceived на C#-стороні.
Це особливо зручно для 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. Ось детальне порівняння за важливими критеріями:
Характеристика
HybridWebView
BlazorWebView
Розмір 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
Debugging
Chrome DevTools через remote inspect
Visual 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:
Після цього залежності контроля не будуть вирізатись, і застосунок продовжить білдитись під 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.
Практичний посібник з навігації Shell у .NET MAUI 10: як реєструвати маршрути, передавати параметри через GoToAsync, налаштувати deep linking через Android App Links і iOS Universal Links та уникнути п'яти найпоширеніших помилок продакшн-проєктів.