O Blazor é uma estrutura de front-end .NET para criar aplicações Web utilizando apenas tecnologias .NET. Em 2021, o Blazor foi estendido ao ambiente desktop com o Blazor Hybrid, permitindo aos programadores utilizar as suas competências existentes em plataformas desktop.

As aplicações Blazor Hybrid são aplicações de ambiente desktop tradicionais que hospedam a aplicação Blazor web real dentro de uma webview de controle. Eles utilizam o .NET MAUI para o ambiente desktop, mas agora você pode utilizar outra estrutura se esta não satisfizer os seus requisitos.

A falta de suporte para Linux e a utilização de diferentes motores de navegadores no Windows e no macOS são limitações da MAUI. O Microsoft Edge e o Safari variam na forma como implementam normas Web, executam JavaScript e processam páginas. Em aplicações avançadas, esta diferença pode ser uma fonte de erros e exigir testes adicionais.

Se a MAUI não for uma opção, considere optar pela Avalonia UI - uma biblioteca UI multiplataforma com várias WebViews baseadas no Chromium em seu ecossistema.

Neste artigo, exploramos como usar o Avalonia UI para criar aplicativos Blazor Hybrid com o DotNetBrowser como uma WebView.

Início rápido com um modelo 

Para criar um aplicativo Blazor Hybrid básico com DotNetBrowser e Avalonia UI, use nosso modelo:

dotnet new install DotNetBrowser.Templates

Em seguida, obtenha uma licença de avaliação gratuita de 30 dias para o DotNetBrowser.

Spinner

Enviando…

Desculpe, o envio foi interrompido

Tente novamente. Se o problema persistir, contate-nos através do endereço info@teamdev.com.

Leia e aceite os termos para continuar.

A sua chave de avaliação pessoal de DotNetBrowser e o guia de início rápido chegarão à sua caixa de entrada de e-mail dentro de alguns minutos.

Crie um aplicativo Blazor Hybrid a partir do modelo e passe sua chave de licença como um parâmetro:

dotnet new dotnetbrowser.blazor.avalonia.app -o Blazor.AvaloniaUi -li <your_license_key>

E execute a aplicação:

dotnet run --project Blazor.AvaloniaUi

Aplicativo Blazor Hybrid na interface do usuário do Avalonia no Linux

Aplicativo Blazor Hybrid no Avalonia UI no Linux.

Implementação 

No ambiente híbrido, o aplicativo Blazor é executado no processo do shell do desktop. Esse shell, ou uma janela, gerencia o ciclo de vida de todo o aplicativo, exibe a WebView e inicia o aplicativo Blazor. Vamos criar essa janela com Avalonia UI.

O back-end da aplicação Blazor é o código .NET e o front-end é o conteúdo Web hospedado dentro de uma WebView. O motor do navegador dentro de uma WebView e o tempo de execução .NET não têm uma ligação direta. Portanto, para que o back end e o front end se comuniquem, o Blazor deve saber como trocar dados entre eles. Estamos introduzindo uma nova WebView, portanto, precisamos ensinar ao Blazor como fazer isso com o DotNetBrowser.

Em seguida, mostraremos as peças-chave que integram o Blazor com o Avalonia e o DotNetBrowser. Consulte o modelo acima para obter a solução completa.

Criar uma janela 

Para hospedar um aplicativo Blazor Hybrid, precisamos criar uma janela normal do Avalonia com um componente WebView.

MainWindow.axaml

<Window ... Closed="Window_Closed">
    <browser:BlazorBrowserView x:Name="BrowserView" ... />
        ...
    </browser:BlazorBrowserView>
</Window>

MainWindow.axaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
	 ...	
        BrowserView.Initialize();
    }

    private void Window_Closed(object sender, EventArgs e)
    {
        BrowserView.Shutdown();
    }
}

O BlazorBrowserView é um controle do Avalonia que criamos para encapsular o DotNetBrowser. Mais adiante o integraremos com o Blazor neste controle.

BlazorBrowserView.axaml

<UserControl ...>
    ...
    <avaloniaUi:BrowserView x:Name="BrowserView" IsVisible="False" ... />
</UserControl>

BlazorBrowserView.axaml.cs

public partial class BlazorBrowserView : UserControl
{
    private IEngine engine;
    private IBrowser browser;

    public BlazorBrowserView()
    {
        InitializeComponent();
    }

    public async Task Initialize()
    {
        EngineOptions engineOptions = new EngineOptions.Builder
        {
            RenderingMode = RenderingMode.HardwareAccelerated
        }.Build();
        engine = await EngineFactory.CreateAsync(engineOptions);
        browser = engine.CreateBrowser();
        ...
        Dispatcher.UIThread.InvokeAsync(ShowView);
    }

    public void Shutdown()
    {
        engine?.Dispose();
    }

    private void ShowView()
    {
        BrowserView.InitializeFrom(browser);
        BrowserView.IsVisible = true;
        browser?.Focus();
    }
}

Configurando o Blazor 

Em aplicações híbridas, a principal entidade responsável pela integração entre o Blazor e o ambiente é o WebViewManager. Esta é uma classe abstrata, então criamos nossa própria implementação chamada BrowserManager e a instanciamos emBlazorBrowserView.

BrowserManager.cs

class BrowserManager : WebViewManager
{
    private static readonly string AppHostAddress = "0.0.0.0";
    private static readonly string AppOrigin = $"https://{AppHostAddress}/";
    private static readonly Uri AppOriginUri = new(AppOrigin);

    private IBrowser Browser { get; }

    public BrowserManager(IBrowser browser, IServiceProvider provider,
                          Dispatcher dispatcher,
                          IFileProvider fileProvider,
                          JSComponentConfigurationStore jsComponents,
                          string hostPageRelativePath)
        : base(provider, dispatcher, AppOriginUri, fileProvider, jsComponents,
               hostPageRelativePath)
    {
        Browser = browser;
    }
    
    ...
}

BlazorBrowserView.axaml.cs

public partial class BlazorBrowserView : UserControl
{
    private IEngine engine;
    private IBrowser browser;
    private BrowserManager browserManager;

    ...

    public async Task Initialize()
    {
        EngineOptions engineOptions = new EngineOptions.Builder
        {
            RenderingMode = RenderingMode.HardwareAccelerated
        }.Build();
        engine = await EngineFactory.CreateAsync(engineOptions);
        browser = engine.CreateBrowser();
        ...
        browserManager = new BrowserManager(browser, ...);
        ...
    }
    ...
}

Um aplicativo Blazor requer um ou mais componentes raiz. Nós os adicionamos ao WebViewManager quando a WebView estiver sendo inicializada.

RootComponent.cs

public class RootComponent
{
    public string ComponentType { get; set; }
    public IDictionary<string, object> Parameters { get; set; }
    public string Selector { get; set; }

    public Task AddToWebViewManagerAsync(BrowserManager browserManager)
    {
        ParameterView parameterView = Parameters == null
                                          ? ParameterView.Empty
                                          : ParameterView.FromDictionary(Parameters);
        return browserManager?.AddRootComponentAsync(
                Type.GetType(ComponentType)!, Selector, parameterView);
    }
}

BlazorBrowserView.axaml.cs

public partial class BlazorBrowserView : UserControl
{
    private IEngine engine;
    private IBrowser browser;
    private BrowserManager browserManager;
    public ObservableCollection<RootComponent> RootComponents { get; set; } = new();
    ...
    public async Task Initialize()
    {
        ...
        engine = await EngineFactory.CreateAsync(engineOptions);
        browser = engine.CreateBrowser();
        browserManager = new BrowserManager(browser, ...);
        
        foreach (RootComponent rootComponent in RootComponents)
        {
            await rootComponent.AddToWebViewManagerAsync(browserManager);
        }
        ...
    }
    ...
}

MainWindow.axaml

<Window ... Closed="Window_Closed">
    <browser:BlazorBrowserView x:Name="BrowserView" ... />
        <browser:BlazorBrowserView.RootComponents>
           <browser:RootComponent Selector="..." ComponentType="..." />
        </browser:BlazorBrowserView.RootComponents>
    </browser:BlazorBrowserView>
</Window>

Carregamento de recursos estáticos 

Numa aplicação Web normal, o navegador carrega páginas e recursos estáticos efetuando requests HTTP a um servidor. Em um aplicativo Blazor Hybrid, ele funciona da mesma forma, mas não há servidor. Ao invés disso, o WebViewManager fornece um método chamadoTryGetResponseContent que recebe um URL e retorna dados como uma resposta quase HTTP.

Enviamos requests e respostas HTTP para este método e vice-versa, interceptando o tráfego HTTPS no DotNetBrowser.

BlazorBrowserView.axaml.cs

public partial class BlazorBrowserView : UserControl
{
    private IEngine engine;
    private IBrowser browser;
    private BrowserManager browserManager;
    ...

    public async Task Initialize()
    {
        EngineOptions engineOptions = new EngineOptions.Builder
        {
            RenderingMode = RenderingMode.HardwareAccelerated,
            Schemes =
            {
                {
                    Scheme.Https,
                    new Handler<InterceptRequestParameters,
                        InterceptRequestResponse>(OnHandleRequest)
                }
            }
        }.Build();

        engine = await EngineFactory.CreateAsync(engineOptions);
        browser = engine.CreateBrowser();
        browserManager = new BrowserManager(browser, ...);
        ...
    }

    public InterceptRequestResponse OnHandleRequest(
            InterceptRequestParameters params) =>
            browserManager?.OnHandleRequest(params);

    ...
}

BrowserManager.cs

internal class BrowserManager : WebViewManager
{
    private static readonly string AppHostAddress = "0.0.0.0";
    private static readonly string AppOrigin = $"https://{AppHostAddress}/";
    private static readonly Uri AppOriginUri = new(AppOrigin);

    ...

    public InterceptRequestResponse OnHandleRequest(InterceptRequestParameters p)
    {
        if (!p.UrlRequest.Url.StartsWith(AppOrigin))
        {
            // Se o pedido não começar com AppOrigin, deixe-o passar.
            return InterceptRequestResponse.Proceed();
        }

        ResourceType resourceType = p.UrlRequest.ResourceType;
        bool allowFallbackOnHostPage = resourceType is ResourceType.MainFrame
                                           or ResourceType.Favicon
                                           or ResourceType.SubResource;

        if (TryGetResponseContent(p.UrlRequest.Url, allowFallbackOnHostPage,
                                  out int statusCode, out string _,
                                  out Stream content,
                                  out IDictionary<string, string> headers))
        {
            UrlRequestJob urlRequestJob = p.Network.CreateUrlRequestJob(p.UrlRequest,
             new UrlRequestJobOptions
             {
                 HttpStatusCode = (HttpStatusCode)statusCode,
                 Headers = headers
                          .Select(pair => new HttpHeader(pair.Key, pair.Value))
                          .ToList()
             });
            Task.Run(() =>
            {
                using (MemoryStream memoryStream = new())
                {
                    content.CopyTo(memoryStream);
                    urlRequestJob.Write(memoryStream.ToArray());
                }

                urlRequestJob.Complete();
            });
            return InterceptRequestResponse.Intercept(urlRequestJob);
        }

        return InterceptRequestResponse.Proceed();
    }
}

Agora, quando a WebView pode navegar para as páginas da aplicação e carregar recursos estáticos, nós podemos carregar a página de índice e ensinar o WebViewManager a realizar a navegação.

BlazorBrowserView.axaml.cs

public partial class BlazorBrowserView : UserControl
{
    private IEngine engine;
    private IBrowser browser;
    private BrowserManager browserManager;
    ...

    public async Task Initialize()
    {
        ...
        engine = await EngineFactory.CreateAsync(engineOptions);
        browser = engine.CreateBrowser();
        browserManager = new BrowserManager(browser, ...);
        
        foreach (RootComponent rootComponent in RootComponents)
        {
            await rootComponent.AddToWebViewManagerAsync(browserManager);
        }
        
        browserManager.Navigate("/");
        ...
    }
    ...
}

BrowserManager.cs

internal class BrowserManager : WebViewManager
{
    ...
    private IBrowser Browser { get; }
    ...

    protected override void NavigateCore(Uri absoluteUri)
    {
        Browser.Navigation.LoadUrl(absoluteUri.AbsoluteUri);
    }
}

Troca de dados 

Ao contrário dos aplicativos da Web comuns, o Blazor Hybrid não usa HTTP para a troca de dados. O front end e o back end comunicam com mensagens de strings utilizando uma interoperabilidade especial .NET-JavaScript. Em JavaScript, as mensagens são enviadas e recebidas através do do objeto window.external, e do lado .NET, através do WebViewManager.

Utilizamos a ponte DotNetBrowser .NET-JavaScript para criar o objeto window.external e transferir as mensagens.

BrowserManager.cs

internal class BrowserManager : WebViewManager
{
    ...
    private IBrowser Browser { get; }
    private IJsFunction sendMessageToFrontEnd;

    public BrowserManager(IBrowser browser, IServiceProvider provider,
                          Dispatcher dispatcher,
                          IFileProvider fileProvider,
                          JSComponentConfigurationStore jsComponents,
                          string hostPageRelativePath)
        : base(provider, dispatcher, AppOriginUri, fileProvider, jsComponents,
               hostPageRelativePath)
    {
        Browser = browser;
        // Este handler é chamado depois que a página é carregada
        // mas antes de executar seu próprio JavaScript.
        Browser.InjectJsHandler = new Handler<InjectJsParameters>(OnInjectJs);
    }
    
    ...

    private void OnInjectJs(InjectJsParameters p)
    {
        if (!p.Frame.IsMain)
        {
            return;
        }

        dynamic window = p.Frame.ExecuteJavaScript("window").Result;
        window.external = p.Frame.ParseJsonString("{}");

        // Quando a página chamar esses métodos, o DotNetBrowser fará
        // chamadas de proxy para os métodos .NET.
        window.external.sendMessage = (Action<dynamic>)OnMessageReceived;
        window.external.receiveMessage = (Action<dynamic>)SetupCallback;
    }

    private void OnMessageReceived(dynamic obj)
    {
        this.MessageReceived(new Uri(Browser.Url), obj.ToString());
    }
    
    private void SetupCallback(dynamic callbackFunction)
    {
        sendMessageToFrontEnd = callbackFunction as IJsFunction;
    }
    
    protected override void SendMessage(string message)
    {
        sendMessageToFrontEnd?.Invoke(null, message);
    }
}

Conclusão 

Neste artigo, discutimos o Blazor Hybrid, uma tecnologia .NET para criar aplicações de ambiente desktop com o Blazor.

O Blazor Hybrid usa o .NET MAUI, que possui duas limitações:

  • Não é compatível com Linux.
  • Utiliza diferentes motores de navegação no Windows e no macOS, enquanto a mesma aplicação pode comportar-se e ter um aspecto diferente.

Ao invés disso, sugerimos a utilização do Avalonia UI + DotNetBrowser. Esta combinação fornece o suporte completo para Windows, macOS e Linux, e garante um ambiente de navegador consistente em todas as plataformas.