| name | maui-hybridwebview |
| description | Guidance for embedding web content in .NET MAUI apps using HybridWebView, including JavaScript–C# interop, bidirectional communication, raw messaging, and trimming/NativeAOT considerations. USE FOR: "HybridWebView", "JavaScript interop", "embed web content", "JS to C# interop", "C# to JavaScript", "web view interop", "raw message", "InvokeJavaScriptAsync", "web content MAUI". DO NOT USE FOR: deep linking from external URLs (use maui-deep-linking), REST API calls (use maui-rest-api), or Blazor Hybrid apps (different from HybridWebView).
|
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:
<script src="_hwv/HybridWebView.js"></script>
<script src="scripts/app.js"></script>
<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:
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext { }
Rule: 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
myHybridWebView.SetInvokeJavaScriptTarget(new MyJsBridge());
⚠️ 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:
try
{
var result = await myHybridWebView.InvokeJavaScriptAsync<string>(
"riskyFunction", MyJsonContext.Default.String);
}
catch (Exception ex)
{
Debug.WriteLine($"JS error: {ex.Message}");
}
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
JsonSerializerIsReflectionEnabledByDefault to false
- Using
JsonSerializerContext as 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