I’ve seen various demos on hosting Blazor applications inside MAUI apps, and I’ve been wondering about the use cases – on the first look it didn’t seem so practical to me. However, a bit later I noticed that having such thing few years ago, it would save us quite a lot of work.
At RIGANTI, we’ve been building several desktop applications for our customers. One of the application contained quite complex reservation system. The application was built using WPF as it needed to interact with various hardware devices (RFID readers, receipt printers, credit card terminals, and so on), and so the reservation calendar (a complex component) was built using WPF.
Later we found that we need a similar calendar on the web – the project has another public facing site which would take advantage of the reservation system, and thus we had to rebuild similar UI on the web. A hybrid app would help us to save a lot of time.
How to integrate web app into MAUI app
I’ve looked at the BlazorWebView control implementation and found out that it’s not complicated – basically it is a wrapper over the WebView2 component. It would be nice to offer a similar feature to DotVVM users. DotVVM apps use the MVVM pattern, same as in MAUI, so it may be even more interesting – both desktop and web UI can use the same Model-View-ViewModel approach.
The implementation in Blazor directly invokes the components to render the HTML. It is not invoking the entire HTTP request pipeline in ASP.NET Core, which can be more performant, however the application may not take advantage of some parts of ASP.NET Core, e. g. the authorization.
Also, there are some custom mechanisms to serve static files to the web app. I haven’t explored the details on Android an iOS yet, so it may be necessary to do it that way, but the static pages middleware in ASP.NET Core could do the same job if it gets the correct implementation of IFileProvider.
Since the DotVVM runtime has a lot of dependencies on ASP.NET Core services, I’ve found that the easiest way would be to invoke directly the RequestDelegate to handle the entire HTTP request using ASP.NET Core pipeline.
Same as in BlazorWebView, the app is running on a special origin 0.0.0.0 – it’s not a valid IP address, but WebView thinks that it is fine, and doesn’t do any DNS resolution. We could use localhost, but if the WebView would need to call something on the local machine, there could be conflicts or some problems with sharing cookies. This seems more feasible.
The crucial part for integration is to intercept the HTTP request sent by the WebView and provide the response without performing any networking operation.
_webview.CoreWebView2.AddWebResourceRequestedFilter($"{AppOrigin}*", CoreWebView2WebResourceContext.All);
_webview.CoreWebView2.WebResourceRequested += async (s, eventArgs) =>
{
…
};
The request is handled like this:
public async Task<DotvvmResponse> ProcessRequest(Uri requestUri, string method, IEnumerable<KeyValuePair<string, string>> headers, Stream contentStream)
{
using var scope = serviceScopeFactory.CreateScope();
var httpContext = new DefaultHttpContext()
{
Request =
{
Scheme = requestUri.Scheme,
Host = new HostString(requestUri.Host, requestUri.Port),
Path = requestUri.PathAndQuery,
Method = method
},
Response =
{
Body = new MemoryStream()
},
ServiceScopeFactory = serviceScopeFactory,
RequestServices = scope.ServiceProvider
};
foreach (var header in headers)
{
httpContext.Request.Headers.Add(header.Key, header.Value);
}
if (contentStream != null)
{
httpContext.Request.Body = contentStream;
}
await requestDelegate(httpContext);
return new DotvvmResponse(
httpContext.Response.StatusCode,
httpContext.Response.Headers.SelectMany(h => h.Value.Select(v => new KeyValuePair<string, string>(h.Key, v))),
(MemoryStream)httpContext.Response.Body);
}
You can obtain the RequestDelegate using the standard ASP.NET Core dependency injection – it is registered there.
Communication with the host
The hosted web app will probably need to communicate with the MAUI app. Since both MAUI and DotVVM runs in the same process, it is theoretically possible to directly call methods in MAUI/DotVVM viewmodels, but DotVVM was designed as stateless and thus its C# viewmodel exists only for a short period of time – only during execution of a particular HTTP request. Most of the time, the viewmodel is represented by the JavaScript object in the page itself.
Therefore, the DotvvmWebView implementation contains several useful methods:
- GetViewModelSnapshot – this method obtains a snapshot of the viewmodel and returns it as dynamic. I’ve been thinking about a strongly-typed way of obtaining the viewmodel, but there are many problems – using the same viewmodel class is not a good idea since it has many dependencies (on HttpContext and other services) – there would have been some interface. Dynamic might be good enough.
- PatchViewModel – the MAUI app can send patches to the page viewmodel. Again, this is fully dynamic API, but since we are working with JavaScript, it’s probably not such a big deal.
- CallNamedCommand – this function allows to call commands declared using <dot:NamedCommand> control in the page. Currently, you need to specify the viewId, which is a DotVVM internal ID of the associated JS module – I want to introduce some API that will get the viewId using a CSS selector – you would be able to specify CSS selector for a control that contains the named command.
DotVVM can send events to the MAUI app by calling _webview.SendNotification(methodName, arg1, arg2…) in a static command – this will invoke the PageNotificationReceived event on DotvvmWebView. Using this mechanism, you can respond to the events in DotVVM pages.
I’ve implemented the prototype of DotVVM + MAUI integration on GitHub. Currently, it supports only Windows UWP host, but I plan to add support for more platforms. There are some further challenges in making this component production-ready – DotVVM compiles its pages on the first access, which will be slow on mobile devices – we’ll probably need some way of pre-compilation.