Implementing Hybrid Development with Vue in .NET MAUI

Implementing Hybrid Development with Vue in .NET MAUI

In MAUI, Microsoft's official solution is to use Blazor development, but most current Web projects are built with Vue, React, etc. If we cannot bypass the accumulated technology, rewriting the entire project with Blazor is not realistic.

Last updated 1/18/2022 10:11 PM
林 小
11 min read
Category
MAUI
Tags
.NET C# Blazor MAUI Vue

In MAUI, Microsoft's official solution is to use Blazor for development. However, most current Web projects in the market are built with technologies like Vue and React. If we cannot bypass the existing technical accumulation, rewriting the entire project with Blazor is not realistic.

Vue is a popular web framework—essentially a template engine that leverages the two core features of "templates" and "binding" to implement the MVVM pattern for web pages. Using the .NET MAUI framework, Vue applications can be embedded into a Web container, enabling cross-platform hybrid development.

For example, in a medical industry project I worked on, I used this hybrid development approach to generate the application. The Vue code required almost no modifications to run across platforms:

If you have a website built with Vue, you can follow this article to try porting it to mobile devices such as iPhones, Android phones, and tablets.

The core work of hybrid development is to build interoperability between Web and .NET. We will utilize the following features of the Blazor engine:

  • Unified resource management
  • JavaScript code injection
  • JavaScript calling C# code
  • C# calling JavaScript code

If you are not yet familiar with the concept of hybrid development, please refer back to the previous chapter: [MAUI] Hybrid Development Concept_jevonsflash's Column-CSDN Blog

The entire workflow is divided into the MAUI part, the Vue part, and the hybrid adaptation.

MAUI Part

Create a Maui App project:

You can also create a Maui Blazor App project and name it MatoProject, but this template is primarily centered around Blazor development. Some features are unnecessary, so you’d have to delete many files.

After creation, edit MatoProject.csproj and append .Razor at the end of the Sdk attribute. Visual Studio will automatically install the Microsoft.AspNetCore.Components.WebView.Maui dependency package (note: do not manually add this package via NuGet, or the program may fail to run).

After installation, create a wwwroot folder in the project directory.

This folder will serve as the root directory for the Web part of the hybrid development. This name cannot be arbitrarily chosen—let’s see why.

Open the Microsoft.AspNetCore.Components.WebView.Maui.targets file:

We can see that when the project is built, this library sets the content inside the wwwroot folder as a Maui resource type (MauiAsset). The compiler then packages these contents into the resource folders of each platform based on the MauiAsset tag. For more details on Maui resource types, refer to this article: .NET MAUI – Manage App Resources – Developer Thoughts (egvijayanand.in).

Open MauiProgram.cs and register the BlazorMauiWebView component in the builder. Use the extension method AddBlazorWebView() in the services to add the related Blazor services:

using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.AspNetCore.Components.WebView.Maui;
using Microsoft.Extensions.DependencyInjection;

namespace MatoProject
{
	public static class MauiProgram
	{
		public static MauiApp CreateMauiApp()
		{
			var builder = MauiApp.CreateBuilder();
			builder
				.RegisterBlazorMauiWebView()
				.UseMauiApp<App>()
				.ConfigureFonts(fonts =>
				{
					fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				});
			builder.Services.AddBlazorWebView();
			return builder.Build();
		}
	}
}

Open MainPage.xaml and edit the main page of the native application:

Create a BlazorWebView control that fills the screen, and set HostPage to the Web part's main page index.html:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MatoProject.MainPage"
             xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
             BackgroundColor="{DynamicResource SecondaryColor}">

    <Grid>
        <b:BlazorWebView HostPage="wwwroot/index.html">
            <b:BlazorWebView.RootComponents>
                <b:RootComponent Selector="#blazorapp" x:Name="MainWebView" ComponentType="{x:Type local:Index}" />
            </b:BlazorWebView.RootComponents>
        </b:BlazorWebView>
    </Grid>
</ContentPage>

Create _import.razor:

@using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using
Microsoft.AspNetCore.Components.Routing @using
Microsoft.AspNetCore.Components.Web @using
Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop
@using MatoProject

Vue Part

At this point, we have set up the native development Web container. Next, we need to handle the Vue project:

cd to the project directory and use vue-cli to create a blank Vue project:

You can set it up according to your Vue preferences. For example, I chose a 2.0 project with TypeScript support, ES6 class naming style, etc. Ultimately, it will be bundled via webpack into static assets, so the specifics don’t matter.

Create src/api/fooService.ts and create the following functions:

The window['DotNet'] object will be the interop object injected by MAUI Blazor:

export async function GetAll(data) {
  var result = null;
  await window["DotNet"]
    .invokeMethodAsync("MatoProject", "GetFoo")
    .then((data) => {
      console.log("DotNet method return the value:" + data);
      result = data;
    });
  return result;
}

export async function Add(data) {
  var result = null;
  await window["DotNet"]
    .invokeMethodAsync("MatoProject", "Add", data)
    .then((data) => {
      console.log("DotNet method return the value:" + data);
      result = data;
    });
  return result;
}

Open Home.vue and edit:

This is the main page of the Web part. We need three buttons and related functions to test the interaction between JavaScript and C#.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <div>
      <h3>foo:</h3>
      <button @click="getFoo">click to get foo</button>
      <br />
      <span>{{ foo }}</span>
    </div>
    <div>
      <h3>bar:</h3>
      <span>{{ bar }}</span>
    </div>
    <div>
      <button @click="add">click here to add</button>
      <span>click count:{{ cnt }}</span>
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
import { GetAll, Add } from "@/api/fooService";

@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {
  foo: string = "";
  bar: string = "";
  cnt: number = 0;

  async created() {
    window["postBar"] = this.postBar;
  }
  async add() {
    this.cnt = await Add({ a: this.cnt, b: 1 });
  }

  async getFoo() {
    var foo = await GetAll(null);
    this.foo = foo;
  }

  async postBar(data) {
    this.bar = data;
    console.log("DotNet invocked the function with param:" + data);
    return this.bar;
  }
}
</script>

At this point, a simple Vue project has been completed.

Run the build command:

PS D:\Project\maui-vue-hybirddev\hybird-host> yarn build

Copy all the contents from the dist directory to the wwwroot folder.

Hybrid Adaptation

This is the key part of hybrid development: modifying the MAUI project to adapt to Vue.

Open wwwroot/index.js and rewrite it as:

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link rel="icon" href="favicon.ico" />
    <title>hybird-host</title>
    <link href="js/about.dc8b0f2b.js" rel="prefetch" />
    <link href="css/app.03043124.css" rel="preload" as="style" />
    <link
      href="js/app.b6b5425b.js"
      rel="preload"
      as="script"
      crossorigin="anonymous"
    />
    <link
      href="js/chunk-vendors.cf6d8f84.js"
      rel="preload"
      as="script"
      crossorigin="anonymous"
    />
    <link href="css/app.03043124.css" rel="stylesheet" />
  </head>
  <body>
    <div id="blazorapp">Loading...</div>
    <script src="_framework/blazor.webview.js" autostart="false"></script>
  </body>
</html>

Note: Rewrite only the body section completely. Do not change the link tags in the head; only add crossorigin="anonymous" to the js scripts to resolve cross-origin issues.

Create the Index.razor file:

@using Microsoft.Maui.Controls @inject IJSRuntime JSRuntime @implements
IDisposable
<noscript
  ><strong
    >We're sorry but CareAtHome doesn't work properly without JavaScript
    enabled. Please enable it to continue.</strong
  ></noscript
>
<div id="app"></div>
@code { [JSInvokable] public static Task<string>
  GetFoo() { return Task.FromResult("this is foo call C# method from js"); }
  [JSInvokable] public static Task<int>
    Add(AddInput addInput) { return Task.FromResult(addInput.a + addInput.b); }
    public async void Post(object o, EventArgs a) { await
    JSRuntime.InvokeAsync<string
      >("postBar", "this is bar call js method from C#"); } protected override
      async Task OnAfterRenderAsync(bool firstRender) { ((App.Current as
      App).MainPage as MainPage).OnPostBar += this.Post; try { if (firstRender)
      { await JSRuntime.InvokeAsync<IJSObjectReference
        >("import", "./js/chunk-vendors.cf6d8f84.js", new { crossorigin =
        "anonymous" }); await JSRuntime.InvokeAsync<IJSObjectReference
          >("import", "./js/app.b6b5425b.js", new { crossorigin = "anonymous"
          }); } } catch (Exception ex) { Console.WriteLine(ex); } } public void
          Dispose() { (Application.Current.MainPage as MainPage).OnPostBar -=
          this.Post; } }

Note: The following two statements must correspond to the actual filenames generated by the build and include the cross-origin attribute:

await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/chunk-vendors.cf6d8f84.js", new { crossorigin = "anonymous" });
await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/app.b6b5425b.js", new { crossorigin = "anonymous" });

In MainPage.xaml, add a button and set the event trigger method:

<button
  Text="Post Bar To WebView"
  HorizontalOptions="Center"
  VerticalOptions="End"
  HeightRequest="40"
  Clicked="PostBar_Clicked"
></button>

Code-behind:

using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Essentials;

namespace MatoProject
{
	public partial class MainPage : ContentPage
	{
        public event EventHandler<EventArgs> OnPostBar;

		int count = 0;

		public MainPage()
		{
			InitializeComponent();
		}

		private async void PostBar_Clicked(object sender, EventArgs args)
		{
			OnPostBar?.Invoke(this, args);
		}
	}
}

At this point, all code work is complete. On a PC, you can choose either a Windows or Android emulator to run the program.

Running result:

If running on the Windows platform, the native control uses the Edge WebView2 renderer to load the page. Pressing F12 will invoke the native debugging tools, where you can see the console output:

Now, some might ask: why use such a technical architecture? There are obviously more convenient hybrid development technologies like Ionic, React Native, and Uni-app. First, it’s undeniable that each of these has its own features and advantages. But when you already have a mature Xamarin framework, you can easily migrate to MAUI, utilize EFCore for data persistence, or integrate the Abp framework to configure dependency injection, global events, localization, and other common mobile development features (another article will teach you how to port Abp into MAUI). Xamarin is a device abstraction layer, and its WebView also has good HTML5 compatibility.

Of course, the main reason is rapid development: your accumulated code is valuable, and minimizing code changes is key. If you are writing Web code using the React technology stack, React Native might be your best choice. There is no optimal technology, only the technology that best suits you.

Code Repositories:

Keep Exploring

Related Reading

More Articles
Same category / Same tag 4/26/2022

Using Masa Blazor in MAUI

Using `.NET MAUI`, you can develop applications that run on `Android`, `iOS`, `macOS`, and `Windows`, Linux (community-supported) from a single shared codebase. One codebase runs on multiple platforms.

Continue Reading