mcpField
M
maui-hybridwebview

maui-hybridwebview

github.com/davidortinauDevelopment17644 installsUpdated 16w ago
Repository

About maui-hybridwebview

A practical reference skill for using .NET MAUI HybridWebView to host local web assets and connect JavaScript with C#. It focuses on interop setup, serialization, exceptions, and common failure modes.

Key features

  1. 01Typed C# and JavaScript interop
  2. 02Raw message exchange
  3. 03Source-generated JSON serialization guidance
  4. 04Trimming and NativeAOT considerations
  5. 05Common setup and runtime troubleshooting

Use cases

  1. 01Debugging failed JavaScript-to-C# calls
  2. 02Choosing typed interop or raw messages
  3. 03Preparing interop code for trimming

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/davidortinau/maui-skills --skill maui-hybridwebview

After installing, type/maui-hybridwebviewin Claude Code to confirm it loaded.

Bundled filesreferences/hybridwebview-api.md

Details

From the project README

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

IssueFix
Blank white screenWeb assets missing from Resources/Raw/wwwroot or DefaultFile not set
JS interop silently failsMissing <script src="_hwv/HybridWebView.js"></script> in HTML
InvokeJavaScriptAsync returns nullReturn type missing [JsonSerializable] attribute in JsonSerializerContext
JS → C# calls do nothingSetInvokeJavaScriptTarget not called before JS invokes C# methods
Serialization crash with trimmingNot 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 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.

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 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

NeedUse
Structured data exchange with type safetyInvokeJavaScriptAsync / InvokeDotNet with JsonSerializerContext
Simple string payloads, fire-and-forgetSendRawMessage / RawMessageReceived
Calling C# from JS with return valuesInvokeDotNet (target must be set first)
Multiple JS functions to callTyped interop — one InvokeJavaScriptAsync per function

Quick checklist

  • Web content is under Resources/Raw/wwwroot
  • index.html includes <script src="_hwv/HybridWebView.js"></script> before app scripts
  • DefaultFile is set (or defaults to index.html)
  • Every interop type has a [JsonSerializable] entry in a JsonSerializerContext
  • SetInvokeJavaScriptTarget is called before JS invokes C# methods
  • InvokeJavaScriptAsync calls are wrapped in try/catch (.NET 9+)
  • If trimming enabled: source-generated JSON serialization configured

Comments

Markdown supported