
Maui Hybridwebview
- 29 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Embeds web content in .NET MAUI apps using HybridWebView with JavaScript-C# interop, bidirectional communication, and raw messaging.
About
Guides embedding web content in .NET MAUI apps via HybridWebView with JavaScript-C# interop, bidirectional communication and raw messaging. A developer uses it when hosting and communicating with web content inside a MAUI app.
- HybridWebView for embedding web content
- JavaScript-C# interop and bidirectional messaging
Maui Hybridwebview by the numbers
- 29 all-time installs (skills.sh)
- Ranked #664 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-hybridwebviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Embeds web content in .NET MAUI apps using HybridWebView with JavaScript-C# interop, bidirectional communication, and raw messaging.
Files
HybridWebView in .NET MAUI
HybridWebView hosts HTML/JS/CSS content inside a .NET MAUI app with bidirectional C#↔JS communication. It is not a general browser control — it is designed for local web content shipped with the app.
Common gotchas
| Issue | Fix |
|---|---|
| Blank white screen | Web assets missing from Resources/Raw/wwwroot or DefaultFile not set |
| JS interop silently fails | Missing <script src="_hwv/HybridWebView.js"></script> in HTML |
InvokeJavaScriptAsync returns null | Return type missing [JsonSerializable] attribute in JsonSerializerContext |
| JS → C# calls do nothing | SetInvokeJavaScriptTarget not called before JS invokes C# methods |
| Serialization crash with trimming | Not using source-generated JsonSerializerContext |
⚠️ Bridge script is mandatory
The HTML page must include the bridge script before any app scripts:
<!-- ✅ Correct order -->
<script src="_hwv/HybridWebView.js"></script>
<script src="scripts/app.js"></script>
<!-- ❌ Wrong — app.js loads before bridge, interop calls fail silently -->
<script src="scripts/app.js"></script>
<script src="_hwv/HybridWebView.js"></script>JSON serialization — every type must be registered
Every parameter type and return type used in InvokeJavaScriptAsync must have a [JsonSerializable] entry:
// ✅ Correct — all interop types registered
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext { }
// ❌ Wrong — adding a new type to interop without registering it
// This causes silent null returns or runtime exceptionsRule: When you add a new type to the interop surface, you must add a [JsonSerializable(typeof(T))] attribute to the context. Forgetting this is the #1 cause of mysterious interop failures.SetInvokeJavaScriptTarget — timing matters
// ✅ Set target BEFORE the web page loads and JS calls C#
myHybridWebView.SetInvokeJavaScriptTarget(new MyJsBridge());
// ❌ Setting it after JS already tried to call — calls are lost⚠️ Call SetInvokeJavaScriptTarget during page construction or OnAppearing, not lazily.
Exception handling (.NET 9+)
JS exceptions thrown during InvokeJavaScriptAsync are forwarded to .NET. Always wrap interop calls:
// ✅ Catches JS errors
try
{
var result = await myHybridWebView.InvokeJavaScriptAsync<string>(
"riskyFunction", MyJsonContext.Default.String);
}
catch (Exception ex)
{
Debug.WriteLine($"JS error: {ex.Message}");
}
// ❌ Unhandled JS exception crashes the interop pipeline
var result = await myHybridWebView.InvokeJavaScriptAsync<string>(
"riskyFunction", MyJsonContext.Default.String);Trimming / NativeAOT pitfalls
Trimming is disabled by default in MAUI projects. If you enable it:
- ⚠️ You must use source-generated
JsonSerializerContext(not reflection-based serialization) - ⚠️ Set
JsonSerializerIsReflectionEnabledByDefaulttofalse - Using
JsonSerializerContextas shown above is recommended regardless of trimming settings
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>Decision framework — typed interop vs raw messages
| Need | Use |
|---|---|
| Structured data exchange with type safety | InvokeJavaScriptAsync / InvokeDotNet with JsonSerializerContext |
| Simple string payloads, fire-and-forget | SendRawMessage / RawMessageReceived |
| Calling C# from JS with return values | InvokeDotNet (target must be set first) |
| Multiple JS functions to call | Typed interop — one InvokeJavaScriptAsync per function |
Quick checklist
- [ ] Web content is under
Resources/Raw/wwwroot - [ ]
index.htmlincludes<script src="_hwv/HybridWebView.js"></script>before app scripts - [ ]
DefaultFileis set (or defaults toindex.html) - [ ] Every interop type has a
[JsonSerializable]entry in aJsonSerializerContext - [ ]
SetInvokeJavaScriptTargetis called before JS invokes C# methods - [ ]
InvokeJavaScriptAsynccalls are wrapped in try/catch (.NET 9+) - [ ] If trimming enabled: source-generated JSON serialization configured
HybridWebView API Reference
Project Layout
Place web assets under Resources/Raw/wwwroot (the default root). Set a different root with the HybridRootComponent property if needed.
Resources/Raw/wwwroot/
index.html ← entry point (default)
scripts/app.js
styles/app.cssXAML Setup
<HybridWebView
x:Name="myHybridWebView"
DefaultFile="index.html"
RawMessageReceived="OnRawMessageReceived"
HorizontalOptions="Fill"
VerticalOptions="Fill" />DefaultFile sets the HTML page loaded on start (defaults to index.html).
index.html Structure
The page must include the bridge script before any app scripts:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /></head>
<body>
<!-- app markup -->
<script src="_hwv/HybridWebView.js"></script>
<script src="scripts/app.js"></script>
</body>
</html>C# → JavaScript (InvokeJavaScriptAsync)
Call a JS function from C# and receive a typed result:
// JS: function addNumbers(a, b) { return a + b; }
var result = await myHybridWebView.InvokeJavaScriptAsync<int>(
"addNumbers",
MyJsonContext.Default.Int32, // return type info
[2, 3], // parameters
[MyJsonContext.Default.Int32, // param 1 type info
MyJsonContext.Default.Int32]); // param 2 type infoFor complex types:
var person = await myHybridWebView.InvokeJavaScriptAsync<Person>(
"getPerson",
MyJsonContext.Default.Person,
[id],
[MyJsonContext.Default.Int32]);JavaScript → C# (InvokeDotNet)
From JS, call a C# method exposed on the invoke target:
const result = await window.HybridWebView.InvokeDotNet('MethodName', [param1, param2]);
window.HybridWebView.InvokeDotNet('LogEvent', ['click', 'button1']); // fire-and-forgetSetting the Invoke Target
Register the object whose public methods JS can call:
myHybridWebView.SetInvokeJavaScriptTarget(new MyJsBridge());
public class MyJsBridge
{
public string Greet(string name) => $"Hello, {name}!";
public Person GetPerson(int id) => new Person { Id = id, Name = "Ada" };
}Method parameters and return values are serialized as JSON.
Raw Messages
For unstructured string communication use raw messages instead of typed interop.
C# → JS:
myHybridWebView.SendRawMessage("payload string");JS → C#:
window.HybridWebView.SendRawMessage('payload string');Receiving in C#:
void OnRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
{
var message = e.Message;
}Receiving in JS:
window.addEventListener('HybridWebViewMessageReceived', e => {
const message = e.detail.message;
});JSON Serialization Setup
Use source-generated JSON serialization. Define a partial context covering every type exchanged between JS and C#:
[JsonSourceGenerationOptions(
WriteIndented = false,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext { }
public class Person
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}JS Exception Forwarding (.NET 9+)
JavaScript exceptions thrown during InvokeJavaScriptAsync are automatically forwarded to .NET as managed exceptions. Wrap calls in try/catch:
try
{
var result = await myHybridWebView.InvokeJavaScriptAsync<string>(
"riskyFunction", MyJsonContext.Default.String);
}
catch (Exception ex)
{
Debug.WriteLine($"JS error: {ex.Message}");
}Trimming and NativeAOT
Trimming and NativeAOT are disabled by default in MAUI projects. If you enable them, ensure JSON source generators are used:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>Using JsonSerializerContext (source generation) as shown above is the recommended pattern regardless of trimming settings.