Invocando Serviços WCF de Páginas Web via JavaScript

Embora o WCF (Windows Communication Foundation) seja uma tecnologia mais antiga, tendo sido proeminente há mais de uma década como a solução de comunicação unificaad da Microsoft para a plataforma .NET, ela foi amlpamente substituída. Desenvolvedores que trabalharam com WCF provavelmente compartilham suas próprias experiências e desafios com essa tecnologia.

Devido a requisitos de projeto recentes, tornou-se necessário utilizar WCF. Para facilitar o uso futuro ou a manutenção de projetos legados, um exemplo prático foi desenvolvido e está disponível para download no final deste artigo.

Cenários de Uso

A seguir, descrevemos o cenário prático que motivou a criação do exemplo demonstrativo:

  • Os clientes exigiram que as contas de usuário do site fossem vinculadas às informações de hardware do computador, garantindo que uma conta pudesse ser usada apenas em máquinas específicas.
  • Solicitou-se que os usuários pudessem interagir com um cliente WinForm executado localmente enquanto usavam o site. Isso envolveria a exibição de módulos de negócios relevantes do cliente.

O cliente WinForm possuía funcionalidades e lógicas de negócios complexas que não eram adequadas para implementação no site. Embora o site e o cliente fossem sistemas de negócios distintos, eles precisavam colaborar para completar o processo de negócios geral. Esta foi uma exigência bastatne peculiar.

O exemplo demonstrativo aborda os seguintes recursos:

  • O WCF é hospedado em um formulário WinForm e expõe interfaces HTTP.
  • Páginas da web invocam interfaces WCF usando JavaScript para controlar o WinForm (o exemplo demonstra a exibição de um formulário pop-up).
  • Páginas da web invocam interfaces WCF usando JavaScript para recuperar informações de hardware do computador (o exemplo recupera o ID da CPU e o endereço MAC).

É importante notar que o site é implantado em um servidor, não na máquina do usuário local. Portanto, para chamar interfaces WCF hospedadas localmente no computador do usuário, o site deve usar JavaScript. Chamar WCF do código de back-end do site não é viável, pois isso introduziria problemas de origem cruzada. A solução implementada utiliza JSONP para requisições GET de origem cruzada para as interfaces WCF, o que atende às necessidades do negócio em questão.

Configuração do Serviço WCF

Crie um novo projeto WinForm (.NET Framework 4 ou superior) e adicione as seguintes referências de assembly DLL:

  • Microsoft.VisualBasic (usado principalmente para garantir a inicialização de instância única do WinForm)
  • System.ServiceModel e System.ServiceModel.Web (necessários para WCF)
  • System.Management (utilizado para obter informações de hardware)

Em seguida, crie as interfaces e as classes de implementação do serviço WCF. Este exemplo demonstra dois serviços WCF:

Interface e Implementação de Teste


using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Windows.Forms;

namespace JobsWcfDemo
{
   [ServiceContract]
   public interface ITest
   {
       /// <summary>
       /// Testa a requisição GET.
       /// Devido a problemas de origem cruzada ao chamar interfaces WCF hospedadas no WinForm via JavaScript,
       /// é necessário usar o método JSONP para requisições.
       /// </summary>
       [OperationContract]
       [WebGet(BodyStyle = WebMessageBodyStyle.WrappedRequest,
               ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
       string GetTest(string p1, string p2);

       /// <summary>
       /// Testa a requisição POST.
       /// O método POST em WebInvoke deve estar em maiúsculas.
       /// Como não há uma solução conhecida para problemas de origem cruzada com serviços WCF hospedados no WinForm,
       /// esta pode ser testada usando ferramentas como Postman.
       /// Alternativamente, o código C# do back-end pode enviar requisições POST com dados JSON.
       /// </summary>
       [OperationContract]
       [WebInvoke(Method = "POST", BodyStyle = WebMessageBodyStyle.WrappedRequest,
                  ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
       string PostTest(string p1, string p2);

       /// <summary>
       /// Exibe o formulário personalizado PopForm no WinForm.
       /// </summary>
       [OperationContract]
       [WebGet(BodyStyle = WebMessageBodyStyle.WrappedRequest,
               ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
       void ShowForm();

       /// <summary>
       /// Exibe uma caixa de mensagem do WinForm com o texto fornecido.
       /// </summary>
       [OperationContract]
       [WebGet(BodyStyle = WebMessageBodyStyle.WrappedRequest,
               ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
       void ShowMessage(string msg);
   }

   [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
   public class TestImpl : ITest
   {
       public string GetTest(string p1, string p2)
       {
           return $"Teste GET bem-sucedido. Parâmetros recebidos: p1={p1}, p2={p2}";
       }

       public string PostTest(string p1, string p2)
       {
           return $"Teste POST bem-sucedido. Parâmetros recebidos: p1={p1}, p2={p2}";
       }

       public void ShowForm()
       {
           PopForm pf = new PopForm();
           pf.Show();
           // Garante que o formulário exibido fique em primeiro plano
           pf.TopMost = true;
           pf.TopMost = false;
       }

       public void ShowMessage(string msg)
       {
           MessageBox.Show(msg, "Mensagem", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
       }
   }
}
 

Interface e Implementação de Computador


using System;
using System.Collections.Generic;
using System.Management;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace JobsWcfDemo
{
   [ServiceContract]
   public interface IComputer
   {
       /// <summary>
       /// Obtém o ID do processador (CPU).
       /// </summary>
       [OperationContract]
       [WebGet(BodyStyle = WebMessageBodyStyle.WrappedRequest,
               ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
       string GetCPU();

       /// <summary>
       /// Obtém o endereço MAC.
       /// </summary>
       [OperationContract]
       [WebGet(BodyStyle = WebMessageBodyStyle.WrappedRequest,
               ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
       string GetMAC();
   }

   [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
   public class ComputerImpl : IComputer
   {
       public string GetCPU()
       {
           List<string> cpus = new List<string>();
           try
           {
               ManagementClass cpuInfo = new ManagementClass("Win32_Processor");
               ManagementObjectCollection cpuCollection = cpuInfo.GetInstances();
               foreach (ManagementObject mo in cpuCollection)
               {
                   string cpuId = mo.Properties["ProcessorId"].Value.ToString().Trim();
                   if (!string.IsNullOrWhiteSpace(cpuId))
                   {
                       cpus.Add(cpuId);
                   }
               }
               return string.Join(",", cpus);
           }
           catch (Exception ex)
           {
               return $"Erro ao obter ID da CPU: {ex.Message}";
           }
       }

       public string GetMAC()
       {
           List<string> macAddresses = new List<string>();
           try
           {
               ManagementClass adapterInfo = new ManagementClass("Win32_NetworkAdapterConfiguration");
               ManagementObjectCollection adapterCollection = adapterInfo.GetInstances();
               foreach (ManagementObject mo in adapterCollection)
               {
                   if ((bool)mo["IPEnabled"] == true)
                   {
                       string mac = mo["MacAddress"].ToString().Replace(":", "-");
                       macAddresses.Add(mac);
                   }
               }
               return string.Join(",", macAddresses);
           }
           catch (Exception ex)
           {
               return $"Erro ao obter endereço MAC: {ex.Message}";
           }
       }
   }
}
 

Em seguida, crie uma classe para gerenciar a criação e o início do serviço WCF:

Classe de Gerenciamento do Serviço WCF


using System;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;

namespace JobsWcfDemo
{
   public class MyWcfService
   {
       private ServiceHost serviceHost;

       /// <summary>
       /// Inicializa e inicia o serviço WCF.
       /// </summary>
       /// <param name="interfaceType">O tipo da interface do serviço.</param>
       /// <param name="implementationType">O tipo da classe que implementa a interface.</param>
       /// <param name="baseAddress">O endereço base do serviço (ex: http://127.0.0.1:9000/nomeDoServico).</param>
       public MyWcfService(Type interfaceType, Type implementationType, string baseAddress)
       {
           serviceHost = new ServiceHost(implementationType);
           var webBinding = new WebHttpBinding
           {
               CrossDomainScriptAccessEnabled = true
           };

           var webBehavior = new WebHttpBehavior
           {
               AutomaticFormatSelectionEnabled = true,
               DefaultOutgoingResponseFormat = WebMessageFormat.Json
           };

           var endpoint = serviceHost.AddServiceEndpoint(interfaceType, webBinding, baseAddress);
           endpoint.Behaviors.Add(webBehavior);

           if (serviceHost.State != CommunicationState.Opened)
           {
               serviceHost.Open();
           }
       }

       /// <summary>
       /// Fecha o serviço.
       /// </summary>
       public void Close()
       {
           if (serviceHost != null && serviceHost.State == CommunicationState.Opened)
           {
               serviceHost.Close();
           }
       }
   }
}
 

Finalmente, no formulário principal do WinForm, inicie dois serviços WCF usando a mesma porta:

Inicialização dos Serviços WCF no WinForm


using System;
using System.Windows.Forms;

namespace JobsWcfDemo
{
   public partial class MainForm : Form
   {
       public MainForm()
       {
           InitializeComponent();
       }

       private MyWcfService testService;
       private MyWcfService computerService;

       private void MainForm_Load(object sender, EventArgs e)
       {
           string serviceBaseUrl = "http://127.0.0.1:9000";

           testService = new MyWcfService(typeof(ITest), typeof(TestImpl), serviceBaseUrl + "/test");
           computerService = new MyWcfService(typeof(IComputer), typeof(ComputerImpl), serviceBaseUrl + "/comp");
       }
   }
}
 

Neste ponto, dois serviços WCF foram configurados para expor interfaces HTTP. Ao iniciar o aplicativo WinForm, os serviços estarão prontos para uso.

Observação importante: Ao hospedar o WCF em um aplicativo WinForm, o Windows 7 pode iniciar o serviço diretamente. No entanto, em versões mais recentes do Windows (como o Windows 10), pode ser necessário executar o aplicativo como administrador para evitar erros. Recomenda-se abrir o projeto no Visual Studio clicando com o botão direito e selecionando "Executar como administrador" para facilitar a depuração e execução.

Para garantir que apenas uma instância do aplicativo WinForm seja executada (evitando conflitos de porta), consulte a documentação sobre inicialização de instância única de aplicativos WinForm. Isso é crucial, pois o aplicativo WinForm precisa iniciar o serviço WCF e escutar em uma porta específica.

Testando as Interfaces do Serviço WCF

Um aplicativo cliente de teste foi desenvolvido especificamente para este projeto. Ele permite testar a invocação de interfaces WCF via JavaScript por meio de uma página HTML estática. O exemplo também inclui uma interface POST do WCF. Como a invocação POST de origem cruzada de JavaScript para WCF não foi totalmente implementada neste exemplo, um trecho de código C# demonstra como enviar uma requisição POST usando WebClient. O código da página de teste é o seguinte:

Página de Teste HTML e JavaScript



<html lang="pt-BR">
<head>
   <meta charset="UTF-8" />
   <title>Teste de Invocação WCF via JS</title>
</head>
<body>
   <h1>Atualmente, a única maneira comprovada de chamar interfaces WCF hospedadas em WinForm via JavaScript é usando requisições GET com JSONP.</h1>
   <h1>As interfaces POST do WCF podem ser testadas usando ferramentas como Postman.</h1>
   <h1>Alternativamente, o JavaScript pode chamar um método do back-end do site, que por sua vez pode invocar a interface POST do WCF usando C#.</h1>
   <h1>Como este é um exemplo de página estática, apenas a requisição GET com JSONP será demonstrada.</h1>

   <fieldset>
       <legend>Teste de Requisição com Parâmetros</legend>
       <br />
       Parâmetro 1: <input type="text" id="p1" /><br />
       Parâmetro 2: <input type="text" id="p2" /><br /><br />
       <input type="button" value="Teste Requisição GET" id="btnGet" />
   </fieldset>
   <hr />
   <fieldset>
       <legend>Teste de Operação de Formulário WinForm</legend>
       <br />
       <input type="button" value="Exibir Formulário Pop-up Personalizado" id="btnPop" /><br /><br />
       Conteúdo da Mensagem: <input type="text" id="p3" />
       <input type="button" value="Exibir Caixa de Mensagem" id="btnMsg" />
   </fieldset>
   <hr />
   <fieldset>
       <legend>Obter Informações de Hardware</legend>
       <br />
       <input type="button" value="Obter ID da CPU" id="btnCPU" /><br /><br />
       <input type="button" value="Obter Endereço MAC" id="btnMAC" />
   </fieldset>

   <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
   <script>
       $(function () {
           // Teste de Requisição GET
           $('#btnGet').click(function () {
               var p1Value = $('#p1').val();
               var p2Value = $('#p2').val();

               $.ajax({
                   // O tipo "get" é implícito para JSONP, então esta linha pode ser omitida.
                   // type: "get",
                   dataType: "jsonp",
                   url: "http://localhost:9000/test/GetTest?p1=" + p1Value + "&p2=" + p2Value,
                   success: function (data) {
                       alert(data);
                   },
                   error: function (err) {
                       alert("Erro na requisição: " + JSON.stringify(err));
                   }
               });
           });

           // Exibir formulário pop-up personalizado
           $('#btnPop').click(function () {
               $.ajax({
                   dataType: "jsonp",
                   url: "http://localhost:9000/test/ShowForm"
               });
           });

           // Exibir caixa de mensagem
           $('#btnMsg').click(function () {
               var p3Value = $('#p3').val();
               $.ajax({
                   dataType: "jsonp",
                   url: "http://localhost:9000/test/ShowMessage?msg=" + encodeURIComponent(p3Value)
               });
           });

           // Obter ID da CPU
           $('#btnCPU').click(function () {
               $.ajax({
                   dataType: "jsonp",
                   url: "http://localhost:9000/comp/GetCPU",
                   success: function (data) {
                       alert("ID da CPU: " + data);
                   },
                   error: function (err) {
                       alert("Erro na requisição: " + JSON.stringify(err));
                   }
               });
           });

           // Obter endereço MAC
           $('#btnMAC').click(function () {
               $.ajax({
                   dataType: "jsonp",
                   url: "http://localhost:9000/comp/GetMAC",
                   success: function (data) {
                       alert("Endereço MAC: " + data);
                   },
                   error: function (err) {
                       alert("Erro na requisição: " + JSON.stringify(err));
                   }
               });
           });
       });
   </script>
</body>
</html>
 

O código C# a seguir demonstra como o cliente pode invocar a interface POST do WCF:

Código Cliente C# para Requisição POST


using System;
using System.Text;
using System.Windows.Forms;
using System.Net;

// Assumindo que este código está em um manipulador de eventos de botão no cliente WinForm.
private void btnPost_Click(object sender, EventArgs e)
{
   // Nota: Newtonsoft.Json não é usado aqui para manter o tamanho do demo pequeno.
   string p1 = tbp1.Text.Trim(); // Supondo que tbp1 seja um controle TextBox
   string p2 = tbp2.Text.Trim(); // Supondo que tbp2 seja um controle TextBox
   string jsonPayload = $"{{\"p1\":\"{p1}\",\"p2\":\"{p2}\"}}";

   using (WebClient client = new WebClient())
   {
       client.Encoding = Encoding.UTF8;
       client.Headers.Add("Content-Type", "application/json");

       try
       {
           string result = client.UploadString("http://localhost:9000/test/PostTest", jsonPayload);
           MessageBox.Show(result);
       }
       catch (Exception ex)
       {
           MessageBox.Show($"Erro na requisição POST: {ex.Message}");
       }
   }
}
 

Para testar, execute o aplicativo cliente WCF (WinForm) primeiro e, em seguida, abra a página HTML estática em um navegador. A página permitirá que você interaja com os serviços WCF.

Embora o WCF seja considerado obsoleto, ele pode ser uma solução viável para certos cenários. Dominar diversas tecnologias oferece mais flexibilidade. O código-fonte completo está disponível para referência detalhada.

Tags: WCF WinForms javascript JSONP REST

Publicado em 8-25 05:30