From maui-skills
Guides embedding web content in .NET MAUI apps using HybridWebView with bidirectional JS-C# interop, serialization setup, common gotchas, and NativeAOT trimming.
npx claudepluginhub davidortinau/maui-skills --plugin maui-skillsThis skill uses the workspace's default tool permissions.
`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.
Guards .NET MAUI projects against deprecated, obsolete, or removed APIs in XAML/C#, Blazor Hybrid, and MauiReactor. Detects target frameworks/library versions and provides replacement patterns for code generation/review/editing.
Guides .NET MAUI safe area and edge-to-edge layouts for .NET 10+ using SafeAreaEdges, SafeAreaRegions, keyboard avoidance on Android, iOS, Mac Catalyst, with Blazor Hybrid support and legacy migrations.
Generates .NET MAUI Shell pages, ViewModels, navigation services, source-generated routes, and dialogs using Shiny MAUI Shell. For MAUI apps with advanced Shell navigation, lifecycle hooks, and tab badges.
Share bugs, ideas, or general feedback.
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.
| 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 |
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>
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 exceptions
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.
// ✅ 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.
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 is disabled by default in MAUI projects. If you enable it:
JsonSerializerContext (not reflection-based serialization)JsonSerializerIsReflectionEnabledByDefault to falseJsonSerializerContext as shown above is recommended regardless of trimming settings<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
| 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 |
Resources/Raw/wwwrootindex.html includes <script src="_hwv/HybridWebView.js"></script> before app scriptsDefaultFile is set (or defaults to index.html)[JsonSerializable] entry in a JsonSerializerContextSetInvokeJavaScriptTarget is called before JS invokes C# methodsInvokeJavaScriptAsync calls are wrapped in try/catch (.NET 9+)