Maio de 2019

Volume 34 – Número 5

[.NET Core 3.0]

Crie um Hub centralizado de solicitação de pull com WinForms no .NET Core 3.0

Por Eric Fleming | Maio de 2019

O Windows Forms, ou simplesmente WinForms, tem sido usado há anos para desenvolver aplicativos poderosos baseados no Windows com interfaces avançadas e interativas. Os investimentos nesses aplicativos da área de trabalho entre empresas de todos os tipos são intensos, com cerca de 2,4 milhões de desenvolvedores usando o Visual Studio para criar aplicativos do estilo de área de trabalho mensalmente. Os benefícios de aproveitar e estender ativos de código existentes do WinForms são atraentes, mas também há outros. A experiência de arrastar e soltar do designer do WinForms permite que os usuários compilem interfaces do usuário totalmente funcionais, sem conhecimento ou treinamento especial. Os aplicativos WinForms são fáceis de implantar e atualizar, podem trabalhar independentemente da conectividade com a Internet e podem oferecer maior segurança, executando em um computador local que não expõe as configurações na Internet. Até recentemente, os aplicativos WinForms só podiam ser criados usando o .NET Framework completo, mas a versão prévia do .NET Core 3.0 muda tudo.

Os novos recursos e benefícios do .NET Core vão além do desenvolvimento para a Web. Com o .NET Core 3.0, o WinForms adiciona funcionalidades, como implantação mais fácil, melhor desempenho, suporte para pacotes NuGet específicos do .NET Core, a interface de linha de comando (CLI) do .NET Core e muito mais. Ao longo deste artigo, abordarei vários desses benefícios, por que eles são importantes e como usá-los em aplicativos WinForms.

Vamos diretamente para a compilação do nosso primeiro aplicativo WinForms do .NET Core 3.0. Neste artigo, vou compilar um aplicativo que recupera e exibe solicitações de pull em aberto de um dos repositórios Microsoft de software livre hospedado no GitHub. A primeira etapa é instalar as versões mais recentes do Visual Studio 2019 e do SDK do .NET Core 3.0 e, depois disso, você terá acesso aos comandos da CLI do .NET Core para criar um novo aplicativo WinForms. Antes da adição do suporte para o .NET Core, isso não era possível para os aplicativos WinForms.

Logo teremos um novo modelo do Visual Studio que permite criar um projeto do WinForms destinado ao .NET Core 3.0. Como ainda não está disponível, vamos gerar um novo projeto do WinForms chamado PullRequestHub executando o seguinte comando:

dotnet new winforms -o PullRequestHub

Para verificar se o projeto foi criado, navegue até o novo diretório criado pelo novo comando dotnet e use a CLI para compilar e executar o projeto, desta forma:

cd .\PullRequestHub\

Como você tem acesso à CLI do .NET Core, também tem acesso aos comandos de restauração (restore), execução (run) e compilação (build). Antes da execução, experimente os comandos de restauração e compilação, da seguinte maneira:

dotnet restore
dotnet build

Esses comandos funcionam como se fossem executados na linha de comando de um aplicativo Web do .NET Core. E observe que, quando você executa o comando dotnet run, na verdade ele executa uma restauração e uma compilação antes de executar o aplicativo (bit.ly/2UCkEaN). Agora, vamos executar o projeto para testá-lo, inserindo dotnet run na linha de comando.

Sucesso! Você acaba de criar seu primeiro aplicativo WinForms do .NET Core. Ao executar, você verá um formulário na tela com o texto "Hello .NET Core!".

Antes que eu me aprofunde com a adição de lógica ao nosso aplicativo, vamos falar sobre o estado atual do modo de exibição Designer do WinForms no Visual Studio.

Configurar o Designer nos aplicativos WinForms do .NET Core

Ao abrir o projeto gerado pela CLI no Visual Studio, talvez você note que algumas funcionalidades estão faltando. Principalmente, notará que não há modo de exibição de designer para aplicativos WinForms do .NET Core. Embora haja planos para disponibilização dessa funcionalidade, eles ainda não foram concluídos.

Felizmente, há uma solução alternativa que pode dar a você acesso a um designer, pelo menos até que o suporte nativo seja adicionado. Por enquanto, você pode criar um projeto do .NET Framework que contenha os arquivos de interface do usuário. Dessa maneira, você poderá editar os arquivos da interface do usuário usando o designer e o projeto do .NET Core fará referência aos arquivos da interface do usuário contidos no projeto do .NET Framework. Assim, você utilizará as funcionalidades da interface do usuário e ainda compilará o aplicativo no .NET Core. Veja como eu fiz isso no meu projeto.

Além do projeto PullRequestHub que você criou, talvez seja conveniente adicionar um novo projeto do WinForms executando uma versão do .NET Full-Framework. Atribua o nome PullRequestHub.Designer ao projeto. Após a criação do novo projeto, remova os arquivos Form1 do projeto do .NET Core, deixando apenas a classe Program.cs.

Navegue até o PullRequestHub.Designer e renomeie os arquivos de formulário para PullRequestForm. Agora, edite o arquivo de projeto do .NET Core e adicione o código a seguir para vincular os arquivos dos dois projetos. Isso se encarregará de quaisquer formulários ou recursos que você criar no futuro, e também:

<ItemGroup>
  <Compile Include=”..\PullRequestHub.Designer\**\*.cs” />
</ItemGroup>

Assim que salvar o arquivo de projeto, você verá o arquivo PullRequestForm no gerenciador de soluções e poderá interagir com ele. Quando você quiser usar o editor de interface do usuário, feche o arquivo PullRequestForm do projeto do .NET Core e abra o arquivo PullRequestForm do projeto do .NET Framework. As alterações serão feitas nos dois arquivos, mas o editor só estará disponível no projeto do .NET Framework.

Compilando o aplicativo

Vamos começar adicionando algum código ao aplicativo. Para recuperar solicitações de pull em aberto do GitHub, preciso criar um HttpClient. É aí que entra o .NET Core 3.0, pois ele fornece acesso ao novo HttpClientFactory. O HttpClient na versão de estrutura inteira tinha alguns problemas, incluindo na criação do cliente com o uso de uma instrução. O objeto HttpClient seria descartado, mas o soquete subjacente não seria liberado por algum tempo, que é de 240 segundos por padrão. Se a conexão de soquete permanecer aberta por 240 segundos e você tiver uma taxa de transferência alta no sistema, há uma chance de o sistema saturar todos os soquetes livres. Quando isso acontece, novas solicitações devem aguardar a liberação de um soquete, o que pode causar alguns impactos drásticos no desempenho.

O HttpClientFactory ajuda a atenuar esses problemas. Por exemplo, ele fornece uma maneira mais fácil de pré-configurar as implementações do cliente em um local mais central. Ele também gerencia o tempo de vida do HttpClients para você, para que não enfrente os problemas mencionados anteriormente. Vamos dar uma olhada em como você pode fazer isso em um aplicativo WinForms.

Uma das melhores e mais fáceis maneiras de usar esse novo recurso é por meio de injeção de dependência. A injeção de dependência ou, de modo geral, inversão de controle, é uma técnica para transferir dependências para classes. Também é uma maneira fantástica de reduzir o acoplamento de classes e facilitar o teste de unidade. Por exemplo, você verá como criar uma instância do IHttpClientFactory enquanto o programa é iniciado, com a possibilidade de usar esse objeto posteriormente no formulário. Não era algo muito fácil de fazer no WinForms em versões anteriores do .NET, e é outra vantagem de usar o .NET Core.

No Program.cs, você criará um método chamado ConfigureServices. Neste método, crie um novo ServiceCollection para disponibilizar os serviços para você por meio da injeção de dependência. Primeiro, você precisará instalar a versão mais recente destes dois pacotes NuGet:

  • “Microsoft.Extensions.DependencyInjection”
  • “Microsoft.Extensions.Http”

Em seguida, adicione o código mostrado na Figura 1. Isso cria um novo IHttpClientFactory para ser usado em seus formulários. O resultado é um cliente que você pode usar explicitamente nas solicitações que envolvam a API do GitHub.

Figura 1 - Criar um novo IHttpClientFactory

private static void ConfigureServices()
{
  var services = new ServiceCollection();
  services.AddHttpClient();
  services.AddHttpClient(“github”, c =>
  {
    c.BaseAddress = new Uri(“https://api.github.com/”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/vnd.github.v3+json”);
    c.DefaultRequestHeaders.Add(“User-Agent”, “HttpClientFactory-Sample”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/json”);
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  });
}

Em seguida, você precisa registrar a classe real do formulário, PullRequestForm, como um singleton. Ao final deste método, adicione a seguinte linha:

services.AddSingleton<PullRequestForm>();

Depois, crie uma instância do ServiceProvider. Na parte superior da classe Program.cs, crie a seguinte propriedade:

private static IServiceProvider ServiceProvider { get; set; }

Agora que você tem uma propriedade para seu ServiceProvider, no final do método ConfigureServices, adicione uma linha para criar o ServiceProvider, assim:

ServiceProvider = services.BuildServiceProvider();

No final de tudo isso, o método ConfigureServices completo deve se parecer com o código na Figura 2.

Figura 2 O método ConfigureServices

private static void ConfigureServices()
{
  var services = new ServiceCollection();
  services.AddHttpClient();
  services.AddHttpClient(“github”, c =>
  {
    c.BaseAddress = new Uri(“https://api.github.com/”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/vnd.github.v3+json”);
    c.DefaultRequestHeaders.Add(“User-Agent”, “HttpClientFactory-Sample”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/json”);
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  });
  services.AddSingleton<PullRequestForm>();
  ServiceProvider = services.BuildServiceProvider();
}

Agora, você precisa conectar o formulário ao contêiner quando ele é iniciado. Quando o aplicativo for executado, ele invocará o PullRequestForm, com os serviços necessários disponíveis. Altere o método Main para o seguinte código:

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  ConfigureServices();
  Application.Run((PullRequestForm)ServiceProvider.GetService(typeof(PullRequestForm)));
}

Ótimo! Agora, você está totalmente conectado. No construtor PullRequestForm, injete o IHttpClientFactory que conectou e atribua-o a uma variável local, conforme mostrado neste código:

private static HttpClient _httpClient;
public PullRequestForm(IHttpClientFactory httpClientFactory)
{
  InitializeComponent();
  _httpClient = httpClientFactory.CreateClient(“github”);
}

Agora, você tem um HttpClient que pode usar para fazer chamadas para o GitHub para recuperar solicitações de pull, problemas e outros. Isso também complicará um pouco as próximas etapas. As chamadas do HttpClient serão solicitações assíncronas e, se você usou o WinForms, sabe o que está por vir. Você terá de lidar com o threading e enviar atualizações de expedição ao thread da interface do usuário.

Para iniciar a recuperação de todas as solicitações de pull, vamos adicionar um botão à exibição. Assim, futuramente, você poderá adicionar mais repositórios ou mais grupos de repositórios para verificação. Usando o designer que você conectou, arraste o botão para o formulário e renomeie o texto para "Microsoft". Enquanto você faz isso, dê um nome mais significativo, como RetrieveData_Button, ao seu botão. Você precisará vincular ao evento RetrieveData_Button_Click, que deverá ser assíncrono, usando este código:

private async void RetrieveData_Button_Click(object sender, EventArgs e)
{
}

É neste ponto que você vai querer chamar um método que recupere solicitações de pull em aberto do GitHub. Mas, primeiro, como está lidando com chamadas assíncronas, você deve conectar o SynchronizationContext. Para isso, adicione uma nova propriedade e atualize o construtor com o seguinte código:

private static HttpClient _httpClient;
private readonly SynchronizationContext synchronizationContext;
public PullRequestForm(IHttpClientFactory httpClientFactory)
{
  InitializeComponent();
  synchronizationContext = SynchronizationContext.Current;
  _httpClient = httpClientFactory.CreateClient(“github”);
}

Em seguida, crie um modelo chamado PullRequestData, para que você possa desserializar facilmente a solicitação. Eis o código para isso:

public class PullRequestData
{
  public string Url { get; set; }
  public string Title { get; set; }
}

Por fim, crie um método chamado GetPullRequestData. Neste método, você fará a solicitação para a API do GitHub e recuperará todas as solicitações de pull em aberto. Você estará desserializando uma solicitação JSON; portanto, adicione a versão mais recente do pacote Newtonsoft.Json ao projeto. Eis o código:

private async Task<List<PullRequestData>> GetPullRequestData()
{
  var gitHubResponse =
    await _httpClient.GetStringAsync(
    $”repos/dotnet/winforms/pulls?state=open”);
  var gitHubData =
    JsonConvert.DeserializeObject<List<PullRequestData>>(gitHubResponse);
  return gitHubData;
}

Ele agora pode ser invocado pelo método RetrieveData_Button_Click. Quando você tiver uma lista de dados desejados, crie uma lista de rótulos para cada título para que possa exibi-lo no formulário. Assim que tiver a lista de rótulos, você poderá adicioná-los à interface do usuário no método UpdateUI. A Figura 3 mostra isso.

Figura 3 Invocar pelo RetrieveData_Button_Click

private async void RetrieveData_Button_Click(object sender, EventArgs e)
{
  var pullRequestData = await GetPullRequestData();
  await Task.Run(() =>
  {
    var labelsToAdd = new List<Label>();
    var verticalSpaceBetweenLabels = 20;
    var horizontalSpaceFromLeft = 10;
    for (int i = 0; i < pullRequestData.Count; i++)
    {
      Label label = new Label();
      label.Text = pullRequestData[i].Title;
      label.Left = horizontalSpaceFromLeft;
      label.Size = new Size(100, 10);
      label.AutoSize = true;
      label.Top = (i * verticalSpaceBetweenLabels);
      labelsToAdd.Add(label);
    }
    UpdateUI(labelsToAdd);
  });
}

Seu método UpdateUI usará o synchronizationContext para atualizar a interface do usuário, da seguinte maneira:

public void UpdateUI(List<Label> labels)
{
  synchronizationContext.Post(new SendOrPostCallback(o =>
  {
    foreach (var label in labels)
    {
      Controls.Add(label);
    }
  }), labels);
}

Se você executar o aplicativo e clicar no botão Microsoft, a interface do usuário será atualizada com os títulos de todas as solicitações de pull em aberto, utilizando o repositório dotnet/winforms no GitHub.

Agora é a sua vez. Para realmente tornar isso um hub centralizado de solicitações de pull, como o título deste artigo promete, vamos atualizar este exemplo para ler vários repositórios de GitHub. Esses repositórios não precisam ser da equipe da Microsoft; de qualquer forma, é divertido acompanhar o progresso deles. Por exemplo, arquiteturas de microsserviços são muito comuns e, nelas, você pode ter vários repositórios que componham o sistema como um todo. Como, geralmente, o ideal é não deixar ramificações e solicitações de pull sem mesclagem por muito tempo, uma ferramenta como essa poderia aumentar sua percepção de solicitações de pull em aberto e melhorar a qualidade de todo o sistema.

Com certeza, você poderia configurar um aplicativo Web, mas então teria de se preocupar com implantações, onde ele sereia executado, autenticação e assim por diante. Com um aplicativo WinForms no .NET Core, você não precisa se preocupar com nada disso. Agora, vamos dar uma olhada em uma das maiores vantagens da criação de um aplicativo WinForms usando o .NET Core.

Empacotamento do aplicativo

No passado, implantar um aplicativo WinForms novo ou atualizado poderia causar problemas relacionados à versão do .NET Framework instalada nos computadores host. Com o .NET Core, os aplicativos podem ser implantados autossuficientes e executados de uma única pasta, sem depender da versão do .NET Framework instalada no computador. Isso significa que o usuário não precisa instalar nada; ele pode simplesmente executar o aplicativo. Também permite atualizar e implantar aplicativos individualmente, pois as versões empacotadas do .NET Core não se afetarão.

Em relação ao aplicativo de exemplo contido neste artigo, você vai querer empacotá-lo como autossuficiente. Lembre-se de que aplicativos autossuficientes serão maiores, pois eles incluem as bibliotecas do .NET Core. Se você estiver implantando em máquinas com as versões mais recentes do .NET Core instaladas, não precisará tornar o aplicativo autossuficiente. Em vez disso, você pode reduzir o tamanho do aplicativo implantado, aproveitando a versão instalada do .NET Core. Opções autossuficientes são usadas quando você não deseja que o aplicativo dependa do ambiente no qual ele será executado.

Para empacotar o aplicativo localmente, verifique se o Modo de Desenvolvedor está habilitado em suas configurações. O Visual Studio avisará e oferecerá um link para as configurações quando você tentar executar o projeto de empacotamento. Mas, para habilitá-lo diretamente, acesse as configurações do Windows, pressionando a tecla Windows e pesquisando por Configurações. Na caixa de pesquisa, digite “Para configurações de desenvolvedores” e selecione-a. Você verá uma opção para habilitar o Modo de Desenvolvedor. Selecione e habilite essa opção.

De maneira geral, as etapas de criação de um pacote autossuficiente parecerão familiares se você já tiver empacotado um aplicativo WinForms. Comece criando um novo Projeto de Empacotamento de Aplicativos do Windows. Atribua ao novo projeto o nome PullRequestHubPackaging. Quando solicitado, selecione o destino e as versões mínimas da plataforma, use os padrões e clique em OK. Clique com o botão direito do mouse em Aplicativos e adicione uma referência ao projeto PullRequestHub.

Após a adição da referência, defina o projeto PullRequestHub como ponto de entrada. Depois disso, a próxima vez que você compilar provavelmente verá o seguinte erro: “O projeto PullRequestHub deve especificar 'RuntimeIdentifiers' no arquivo de projeto quando 'SelfContained' for true”.

Para corrigir esse erro, edite o arquivo PullRequestHub.csproj. Quando você abrir esse arquivo de projeto, observará ainda outra vantagem de usar o .NET Core, pois esse arquivo estará usando o formato novo e leve. Nos projetos do WinForms baseados no .NET Framework, o arquivo de projeto é muito mais detalhado, com padrões explícitos e referências, bem como referências de NuGet divididas em um arquivo packages.config. O formato do novo arquivo de projeto traz referências do pacote para o arquivo de projeto, possibilitando o gerenciamento de todas as suas dependências em um só lugar.

Nesse arquivo, no primeiro nó PropertyGroup, adicione a seguinte linha:

<RuntimeIdentifiers>win-x86</RuntimeIdentifiers>

Um identificador de tempo de execução é usado para identificar as plataformas de destino em que o aplicativo é executado e é usado por pacotes .NET para representar ativos específicos da plataforma em pacotes NuGet. Após a adição, a compilação será realizada e você poderá definir o projeto PullRequestHubPackaging como projeto de inicialização no Visual Studio.

Algo a observar no arquivo PullRequestHubPackaging.wapproj é a definição para indicar que o projeto é autossuficiente. A seção do código no arquivo para se prestar atenção é a seguinte:

<ItemGroup>
  <ProjectReference Include=”..\PullRequestHub\PullRequestHub.csproj”>
    <DesktopBridgeSelfContained>True</DesktopBridgeSelfContained>
    <DesktopBridgeIdentifier>$(DesktopBridgeRuntimeIdentifier)
    </DesktopBridgeIdentifier>
      <Properties>SelfContained=%(DesktopBridgeSelfContained);
        RuntimeIdentifier=%(DesktopBridgeIdentifier)
      </Properties>
    <SkipGetTargetFrameworkProperties>True
    </SkipGetTargetFrameworkProperties>
  </ProjectReference>
</ItemGroup>

Aqui, você pode ver que a opção DesktopBridgeSelfContained está definida como true, o que permite que o aplicativo WinForms seja empacotado com os binários do .NET Core. Quando você executa o projeto, ele despeja os arquivos em uma pasta chamada "win-x86", encontrada em um caminho semelhante a este:

C:\Your-Path\PullRequestHub\PullRequestHub\bin\x86\Debug\netcoreapp3.0

Dentro da pasta win-x86, você notará muitas DLLs, as quais incluem tudo o que o aplicativo autossuficiente precisa para ser executado.

Mais provavelmente, você vai querer implantar o aplicativo como um aplicativo carregado por sideload ou carregá-lo na Microsoft Store. O carregamento por sideload possibilitará atualizações automáticas usando um arquivo appinstaller. Essas atualizações têm suporte a partir da Atualização 15.7 do Visual Studio 2017. Você também pode criar pacotes com suporte de envio para a Microsoft Store para distribuição. A Microsoft Store processa então toda a assinatura de código, distribuição e atualização do aplicativo.

Além dessas opções, há um trabalho em andamento para possibilitar o empacotamento de aplicativos em um único executável, eliminando a necessidade de encher um diretório de saída com DLLs.

Vantagens adicionais

Com o .NET Core 3.0, você também pode aproveitar os recursos do C# 8.0, incluindo tipos de referência que permitem valor nulo, implementações padrão em interfaces, aprimoramentos para alternar instruções usando padrões e fluxos assíncronos. Para habilitar o C# 8.0, abra o arquivo PullRequestHub.csproj e adicione a seguinte linha ao primeiro PropertyGroup:

<LangVersion>8.0</LangVersion>

Outra vantagem de usar o .NET Core e o WinForms é que ambos os projetos são de software livre. Isso fornece acesso ao código-fonte e permite arquivar bugs, compartilhar opiniões e se tornar um colaborador. Confira o projeto do WinForms em github.com/dotnet/winforms.

O .NET core 3.0 promete renovar os investimentos que as empresas fizeram nos aplicativos WinForms, que continuam a ser produtivos, confiáveis e fáceis de implantar e manter. Os desenvolvedores podem aproveitar as novas classes específicas do .NET Core, como HttpClientFactory, utilizar recursos do C# 8.0, como tipos de referência que permitem valor nulo, e empacotar aplicativos autossuficientes. Eles também ganham acesso à CLI do .NET Core e a todas as melhorias de desempenho que acompanham o .NET Core.


Eric Flemingé engenheiro sênior de software com mais de uma década de experiência trabalhando com ferramentas e tecnologias Microsoft. O blog dele é o ericflemingblog.com e ele hospeda conjuntamente o canal do YouTube Function Junction, que explora o Azure Functions. Siga-o no Twitter: @efleming18.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Olia Gavrysh (Microsoft), Simon Timms.


Discuta esse artigo no fórum do MSDN Magazine