.NET Multi-platform App UI (MAUI) — это кроссплатформенная среда пользовательского интерфейса для создания собственных и современных приложений на C#. Это позволяет разработчикам создавать единую кодовую базу для нескольких платформ. Blazor — это фреймворк веб-интерфейса для создания интерактивных веб-приложений на стороне клиента с помощью .NET. Это позволяет разработчикам писать код C#, который запускается в браузере с помощью WebAssembly. При совместном использовании .NET MAUI и Blazor представляют собой мощную комбинацию для создания кроссплатформенных приложений, которые могут работать на нескольких платформах, включая настольные компьютеры, Интернет и мобильные устройства. В этой статье мы покажем, как создать гибридное приложение Blazor с Dynamsoft Barcode SDK. Приложение сможет сканировать линейные и двухмерные штрих-коды в Windows, macOS, iOS и Android. .

Предпосылки

Начало работы с Blazor WebAssembly

Поскольку компоненты пользовательского интерфейса Blazor могут совместно использоваться проектами Blazor WebAssembly и .NET MAUI Blazor, мы начнем с создания проекта Blazor WebAssembly. Для этого откройте Visual Studio 2022 и создайте новый проект Blazor WebAssembly App.

Чтобы сэкономить время на написании кода для веб-считывателя и сканера штрих-кодов, мы будем использовать репозиторий https://github.com/yushulx/javascript-barcode-qr-code-scanner. В этом репозитории представлены примеры, созданные с использованием Dynamsoft JavaScript Barcode SDK.

Шаги по интеграции пакета SDK штрих-кода JavaScript в проект Blazor WebAssembly следующие:

  1. Создайте два компонента Razor в папке Pages: Reader.razor и Scanner.razor.
  2. Скопируйте код пользовательского интерфейса HTML5 из ​​примеров в компоненты Razor.
  • Reader.razor: загрузите файл изображения с помощью компонента InputFile и отобразите изображение в элементе img. Элемент canvas используется для рисования местоположения штрих-кода и текста штрих-кода. Элемент p используется для отображения текста штрих-кода.
@page "/barcodereader"
@inject IJSRuntime JSRuntime

<InputFile OnChange="LoadImage" />
<p class="p-result">@result</p>

<div id="imageview">
    <img id="image" />
    <canvas id="overlay"></canvas>
</div>

@code {
    String result = "";
    private DotNetObjectReference<Reader> objRef;

    private async Task LoadImage(InputFileChangeEventArgs e)
    {
        result = "";

        var imageFile = e.File;
        var jsImageStream = imageFile.OpenReadStream(1024 * 1024 * 20);
        var dotnetImageStream = new DotNetStreamReference(jsImageStream);
        await JSRuntime.InvokeAsync<byte[]>("jsFunctions.setImageUsingStreaming", objRef, "overlay",
        "image", dotnetImageStream);
    }

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    [JSInvokable]
    public void ReturnBarcodeResultsAsync(String text)
    {
        result = text;
        StateHasChanged();
    }

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
  • Scanner.razor: элемент select используется для выбора источника видео. Элемент div используется для отображения видеопотока. Элемент canvas используется для рисования местоположения штрих-кода и текста штрих-кода.
@page "/barcodescanner"

@inject IJSRuntime JSRuntime

<div class="select">
    <label for="videoSource">Video source: </label>
    <select id="videoSource"></select>
</div>

<div id="videoview">
    <div class="dce-video-container" id="videoContainer"></div>
    <canvas id="overlay"></canvas>
</div>

@code {
    String result = "";
    private DotNetObjectReference<Scanner> objRef;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeAsync<Boolean>("jsFunctions.initScanner", objRef, "videoContainer", "videoSource", "overlay");
        }
    }


    [JSInvokable]
    public void ReturnBarcodeResultsAsync(String text)
    {
        result = text;
        StateHasChanged();
    }

    public void Dispose()
    {
        objRef?.Dispose();
    }
}

3. Скопируйте код JavaScript из примеров в файл wwwroot/jsInterop.js.

window.jsFunctions = {
     setImageUsingStreaming: async function setImageUsingStreaming(dotnetRef, overlayId, imageId, imageStream) {
         const arrayBuffer = await imageStream.arrayBuffer();
         const blob = new Blob([arrayBuffer]);
         const url = URL.createObjectURL(blob);
         document.getElementById(imageId).src = url;
         document.getElementById(imageId).style.display = 'block';
         initOverlay(document.getElementById(overlayId));
         if (reader) {
             reader.maxCvsSideLength = 9999
             decodeImage(dotnetRef, url, blob);
         }

     },
     initSDK: async function () {
         if (reader != null) {
             return true;
         }
         let result = true;
         try {
             reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
             await reader.updateRuntimeSettings("balance");
         } catch (e) {
             console.log(e);
             result = false;
         }
         return result;
     },
     initScanner: async function(dotnetRef, videoId, selectId, overlayId) {
         let canvas = document.getElementById(overlayId);
         initOverlay(canvas);
         videoSelect = document.getElementById(selectId);
         videoSelect.onchange = openCamera;
         dotnetHelper = dotnetRef;

         try {
             scanner = await Dynamsoft.DBR.BarcodeScanner.createInstance();
             await scanner.setUIElement(document.getElementById(videoId));
             await scanner.updateRuntimeSettings("speed");

             let cameras = await scanner.getAllCameras();
             listCameras(cameras);
             await openCamera();
             scanner.onFrameRead = results => {
                 showResults(results);
             };
             scanner.onUnduplicatedRead = (txt, result) => { };
             scanner.onPlayed = function () {
                 updateResolution();
             }
             await scanner.show();

         } catch (e) {
             console.log(e);
             result = false;
         }
         return true;
     },
 };

Эти функции JavaScript можно вызывать из компонентов Razor. Параметр dotnetRef используется для вызова методов .NET в компоненте Razor.

4. В файл index.html добавьте следующий код для загрузки SDK Dynamsoft JavaScript Barcode и файла jsInterop.js.

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/dbr.js"></script>
 <script src="jsInterop.js"></script>

5. После этого вы можете запустить приложение Blazor Web Barcode Reader.

Чтобы развернуть проект на GitHub Pages, вы можете использовать следующий файл рабочего процесса:

name: blazorwasm

 on:
   push:
     branches: [ master ]
   pull_request:
     branches: [ master ]

   workflow_dispatch:

 jobs:
   build:
     runs-on: ubuntu-latest

     steps:
       - uses: actions/checkout@v3
          
       - name: Setup .NET Core SDK
         uses: actions/setup-dotnet@v2
         with:
           dotnet-version: '6.0.x'
           include-prerelease: true
              
       - name: Publish .NET Core Project
         run: dotnet publish BlazorBarcodeSample.csproj -c Release -o release --nologo 
            
       - name: Change base-tag in index.html from / to blazor-barcode-qrcode-reader-scanner
         run: sed -i 's/<base href="\/" \/>/<base href="\/blazor-barcode-qrcode-reader-scanner\/" \/>/g' release/wwwroot/index.html
            
       - name: copy index.html to 404.html
         run: cp release/wwwroot/index.html release/wwwroot/404.html
            
       - name: Add .nojekyll file
         run: touch release/wwwroot/.nojekyll
          
       - name: Commit wwwroot to GitHub Pages
         uses: JamesIves/[email protected]
         with:
           GITHUB_TOKEN: $
           BRANCH: gh-pages
           FOLDER: release/wwwroot

Пожалуйста, измените BlazorBarcodeSample.csproj и blazor-barcode-qrcode-reader-scanner в соответствии с именами вашего проекта и репозитория.

Миграция Blazor WebAssembly в .NET MAUI Blazor

Чтобы создать новый проект .NET MAUI Blazor, выполните следующие действия.

  1. Сравните структуру проекта .NET MAUI Blazor со структурой Blazor WebAssembly, чтобы понять сходство.
  2. Скопируйте папки wwwroot и Pages из проекта Blazor WebAssembly в новый проект .NET MAUI Blazor, чтобы быстро запустить его.

Важно отметить, что в отличие от веб-приложений, приложения .NET MAUI Blazor — это нативные приложения, которые изолированы и требуют разрешения пользователя для доступа к камере. Поэтому необходимо добавить следующий код C# в файл Scanner.razor, чтобы запросить разрешение на доступ к камере.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
        if (status == PermissionStatus.Granted)
        {
            isGranted = true;
        }
        else
        {
            status = await Permissions.RequestAsync<Permissions.Camera>();
            if (status == PermissionStatus.Granted)
            {
                isGranted = true;
            }
        }

        if (isGranted)
        {
            StateHasChanged();
            objRef = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeAsync<Boolean>("jsFunctions.initScanner", objRef, "videoContainer", "videoSource", "overlay");
        }
    }
}

Следующим шагом является рассмотрение определенных соображений, связанных с платформой. Поскольку мы работаем с Windows, Android, iOS и macOS, важно отметить, что каждый из них может вести себя по-разному.

Запрос разрешений камеры в .NET MAUI Blazor

Windows

Никаких дополнительных работ не требуется.

Android

  1. Создайте собственный класс WebChromeClient в файле Platforms/Android/MyWebChromeClient.cs:
using Android.Content;
 using Android.Webkit;

 namespace BarcodeScanner.Platforms.Android
 {
     public class MyWebChromeClient : WebChromeClient
     {
         private MainActivity _activity;


         public MyWebChromeClient(Context context)
         {
             _activity = context as MainActivity;
         }

         public override void OnPermissionRequest(PermissionRequest request)
         {
             try
             {
                 request.Grant(request.GetResources());
                 base.OnPermissionRequest(request);
             }
             catch (Exception ex)
             {
                 Console.WriteLine(ex);
             }
         }

         public override bool OnShowFileChooser(global::Android.Webkit.WebView webView, IValueCallback filePathCallback, FileChooserParams fileChooserParams)
         {
             base.OnShowFileChooser(webView, filePathCallback, fileChooserParams);
             return _activity.ChooseFile(filePathCallback, fileChooserParams.CreateIntent(), fileChooserParams.Title);
         }

     }
 }

Вы должны переопределить методы OnPermissionRequest и OnShowFileChooser. Метод OnPermissionRequest используется для предоставления разрешения на доступ к камере. Метод OnShowFileChooser используется для запуска действия по выбору файла.

2. В файле MainActivity.cs добавьте следующий код, чтобы получить возвращенный файл изображения и активировать метод обратного вызова:

public class MainActivity : MauiAppCompatActivity
 {
     private IValueCallback _filePathCallback;
     private int _requestCode = 100;

     protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
     {
         if (_requestCode == requestCode)
         {
             if (_filePathCallback == null)
                 return;

             Java.Lang.Object result = FileChooserParams.ParseResult((int)resultCode, data);
             _filePathCallback.OnReceiveValue(result);
         }
     }

     public bool ChooseFile(IValueCallback filePathCallback, Intent intent, string title)
     {
         _filePathCallback = filePathCallback;

         StartActivityForResult(Intent.CreateChooser(intent, title), _requestCode);

         return true;
     }
 }

3. Создайте файл MauiBlazorWebViewHandler.cs для настройки пользовательского веб-представления:

namespace BarcodeScanner.Platforms.Android
 {
   public class MauiBlazorWebViewHandler : BlazorWebViewHandler
   {

       protected override global::Android.Webkit.WebView CreatePlatformView()
       {
           var view = base.CreatePlatformView();
           view.SetWebChromeClient(new MyWebChromeClient(this.Context));
           return view;
       }
   }
 }

4. Зарегистрируйте MauiBlazorWebViewHandler в файле MauiProgram.cs:

using Microsoft.AspNetCore.Components.WebView.Maui;
 #if ANDROID
 using BarcodeScanner.Platforms.Android;
 #endif

 namespace BarcodeScanner;

 public static class MauiProgram
 {

     public static MauiApp CreateMauiApp()
     {
       var builder = MauiApp.CreateBuilder();
       builder
         .UseMauiApp<App>()
         .ConfigureFonts(fonts =>
         {
           fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
         }).ConfigureMauiHandlers(handlers =>
               {
   #if ANDROID
                   handlers.AddHandler<BlazorWebView, MauiBlazorWebViewHandler>();
   #endif
         });

           builder.Services.AddMauiBlazorWebView();
   #if DEBUG
       builder.Services.AddBlazorWebViewDeveloperTools();
   #endif

       return builder.Build();
     }
 }

iOS

  1. Назовите от BlazorWebView до webView:
<?xml version="1.0" encoding="utf-8" ?>
 <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:BarcodeScanner"
             x:Class="BarcodeScanner.WebContentPage"
             Title="WebContentPage"
       BackgroundColor="{DynamicResource PageBackgroundColor}">
     <BlazorWebView x:Name="webView" HostPage="wwwroot/index.html">
         <BlazorWebView.RootComponents>
             <RootComponent Selector="#app" ComponentType="{x:Type local:WebContent}" />
         </BlazorWebView.RootComponents>
     </BlazorWebView>
 </ContentPage>

2. Настройте свойства WKWebView в соответствующем файле C#:

public partial class WebContentPage : ContentPage
   {
     public WebContentPage()
     {
       InitializeComponent();
         webView.BlazorWebViewInitializing += WebView_BlazorWebViewInitializing;
     }

     private void WebView_BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)
     {
 #if IOS || MACCATALYST                   
             e.Configuration.AllowsInlineMediaPlayback = true;
             e.Configuration.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;
 #endif
     }
   }

macOS

На данный момент доступ к камере невозможен в приложении .NET MAUI Blazor на macOS из-за отсутствия поддержки getUserMedia() в WKWebView.

Создание гибридного приложения для сканирования штрих-кодов с помощью .NET и Web SDK для штрих-кодов

Мы успешно разработали кроссплатформенное приложение для сканирования штрих-кода с использованием .NET MAUI Blazor. Однако логика сканирования штрих-кода реализована в JavaScript, что может повлиять на производительность. Чтобы оптимизировать производительность, рекомендуется использовать собственный SDK штрих-кода .NET, если только нет определенных функций, которые не поддерживаются SDK, например API потока камеры для сценариев обработки изображений.

Для приложений Windows .NET MAUI декодирование штрих-кодов из файлов изображений можно выполнить с помощью страницы содержимого MAUI, а декодирование штрих-кодов из потоков камеры можно выполнить с помощью веб-представления Blazor.

Вот шаги для создания гибридного приложения для сканирования штрих-кода:

  1. Получите существующий пример проекта .NET MAUI со страницы https://github.com/yushulx/dotnet-barcode-qr-code-sdk/tree/main/example/maui. Проект поддерживает декодирование штрих-кодов из файлов изображений и потоков с камер с помощью BarcodeQRCodeSDK, который является собственным SDK штрих-кода .NET. Проект не может сканировать штрих-коды из потоков камеры в Windows из-за отсутствия API камеры .NET MAUI.

2. Измените файл *.csproj, сравнив проект .NET MAUI Blazor:

- <Project Sdk="Microsoft.NET.Sdk">
 + <Project Sdk="Microsoft.NET.Sdk.Razor">

 + <EnableDefaultCssItems>false</EnableDefaultCssItems>

3. Измените файл MauiProgram.cs, чтобы добавить поддержку BlazorWebView:

using Microsoft.Maui.Controls.Compatibility.Hosting;
 using SkiaSharp.Views.Maui.Controls.Hosting;
 using Microsoft.AspNetCore.Components.WebView.Maui;

 namespace BarcodeQrScanner;

 public static class MauiProgram
 {
   public static MauiApp CreateMauiApp()
   {
     var builder = MauiApp.CreateBuilder();
     builder.UseSkiaSharp()
       .UseMauiApp<App>()
       .ConfigureFonts(fonts =>
       {
         fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
         fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
       }).UseMauiCompatibility()
             .ConfigureMauiHandlers((handlers) => {
                    
 #if ANDROID
                 handlers.AddCompatibilityRenderer(typeof(CameraPreview), typeof(BarcodeQrScanner.Platforms.Android.CameraPreviewRenderer));
 #endif

 #if IOS
                                 handlers.AddHandler(typeof(CameraPreview), typeof(BarcodeQrScanner.Platforms.iOS.CameraPreviewRenderer));
 #endif
       });

     builder.Services.AddMauiBlazorWebView();
 #if DEBUG
     builder.Services.AddBlazorWebViewDeveloperTools();
 #endif

     return builder.Build();
   }
 }

4. Скопируйте папки wwwroot, Pages, Shared из проекта .NET MAUI Blazor в проект .NET MAUI.

5. В проекте .NET MAUI Blazor переименуйте Main.razor в WebContent.razor и обновите код:

<Router AppAssembly="@typeof(WebContent).Assembly">
   <Found Context="routeData">
     <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
         <FocusOnNavigate RouteData="@routeData" Selector="h1" />
   </Found>
   <NotFound>
     <LayoutView Layout="@typeof(MainLayout)">
       <p role="alert">Sorry, there's nothing at this address.</p>
     </LayoutView>
   </NotFound>
 </Router>

Переименуйте MainPage.xaml в WebContentPage.xaml и обновите код:

<?xml version="1.0" encoding="utf-8" ?>
 <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:BarcodeQrScanner"
             x:Class="BarcodeQrScanner.WebContentPage"
             Title="WebContentPage"
       BackgroundColor="{DynamicResource PageBackgroundColor}">
     <BlazorWebView x:Name="webView" HostPage="wwwroot/index.html">
         <BlazorWebView.RootComponents>
             <RootComponent Selector="#app" ComponentType="{x:Type local:WebContent}" />
         </BlazorWebView.RootComponents>
     </BlazorWebView>
 </ContentPage>

6. Скопируйте WebContent.razor, WebContentPage.xaml и WebContentPage.xaml.cs в проект .NET MAUI.

7. В файле MainPage.xaml.cs добавьте следующий код для перехода к файлу WebContentPage:.

async void OnTakeVideoButtonClicked(object sender, EventArgs e)
 {
     if (DeviceInfo.Current.Platform == DevicePlatform.WinUI || DeviceInfo.Current.Platform == DevicePlatform.MacCatalyst)
     {
         await Navigation.PushAsync(new WebContentPage());
         return;
     }

     var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
     if (status == PermissionStatus.Granted)
     {
         await Navigation.PushAsync(new CameraPage());
     }
     else
     {
         status = await Permissions.RequestAsync<Permissions.Camera>();
         if (status == PermissionStatus.Granted)
         {
             await Navigation.PushAsync(new CameraPage());
         }
         else
         {
             await DisplayAlert("Permission needed", "I will need Camera permission for this action", "Ok");
         }
     }
 }

8. Теперь приложение Windows .NET MAUI может декодировать штрих-коды из файлов изображений и потоков камер с использованием .NET и веб-API соответственно.

Исходный код

Первоначально опубликовано на https://www.dynamsoft.com 12 апреля 2023 г.