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.
Enviando…
Desculpe, o envio foi interrompido
Tente novamente. Se o problema persistir, contate-nos através do endereço info@teamdev.com.
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
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();
}
}
Navegação
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.