Em dezembro de 2017 separei um notebook que não estava mais usando e tomei o caminho de instalar um linux para avaliar o quando seria possível viver sem Windows e sem uma máquina virtual (rsrs).
Como todos sabem, a principal ferramenta de trabalho que utilizo é o Delphi – então o desafio ficou imenso – já que sem uma máquina virtual e sem o windows é impossível utilizar o Delphi. Usando outro framework para substituir o Delphi não iria proporcionar emoção – então descartado frameworks na mesma linha.
Primeiro ponto, o que usar para desenvolver aplicativos ? Como bom delpheiro JAVA não estava nos meus planos, queria algo mais novo.

Primeira parada – se atualizar
Desktop, WEB, Mobile – são opções de plataforma de trabalho. Estacionei no item WEB, vamos procurar o que temos para explorar aí.
Já havia utilizado o Angular-JS a alguns anos para implementar algumas coisas, então vamos nos atualizar no que há de mais novo de Angular – Angular 5 (logo no início do ano veio o 6), tudo novo – esquece o Angular-JS – agora é Typescript.
Typescript é uma implementação feito utilizando como base o javascript – você escreve Typescript e o compilador gera código javascript para ser utilizada no Browser – nem tudo precisa iniciar do zero – legal.

No AngularIO, novas ferramentas de geração e compilação vieram no pacote:

  • o NG (a ferramento do AngularIO) agora gera compilações para serem publicadas no servidor do SITE
  • compila para teste local na máquina com um mecanismo de “Hot Load” muito eficiente – ficou muito bom
  • é muito produtivo para criação de páginas dinâmicas
  • com uso de NodeJS é possível escrever servidores para publicar serviços (ex: busca em banco de dados)

Ferramentas
Para trabalhar com AngularIO, não é preciso ter um windows rodando – Assim como no windows, no linux temos disponível o VS – Virtual Code Studio da MS que faz o trabalho com perfeição, primeira etapa concluída – já é possível seguir o trabalho diário sem Windows.

Preparação de Infra-estrutura
Primeiro passo a resolver é fazer a integração do código Typescript para consumir os serviços de REST em produção escritos em DELPHI. Para isto utilizei o ODataBr (parte do pacote MVCBr – vários artigos aqui no blog – MVCBr).

  • Criado uma camada base de comunicação com o servidor – a parte de http, autenticação, tradução de protocolo, tratamento de “Future” ou respostas assíncronas
  • Criado camada de comandos com classe que tenham regrar de tratamento do protocolo OData
  • Criado camada de classes de acesso aos serviços do servidor utilizados pela aplicação

Produtos com AngularIO

  • Criado um Dashboard para os aplicativos Delphi que estavam em fase de produção para lançamento em 2018
  • Treinamento de equipe para desenvolvimento em AngularIO
  • Implementação da plataforma NcConstruir – com portal de interação do logista com a plataforma – ver: Nc Construir
  • Implementado uma experiência de venda de produto por Mobile rodando em WebView;

Segunda parada – Google Firebase
…. próximo

A versão 2 do ODataBr (antigo MVCBrODataServer) recebeu novos recursos de melhoria de performance e outros de funcionalidades como segue.

1. Melhorias
– algumas propriedades do FireDAC foram alteradas visando a melhoria de performace – alterações de propriedades;

2. Correções
– Foi feito correção para tabelas JOIN

exemplo: http://localhost:8080/OData/OData.svc/cliente(1)/vendas

no exemplo é feito um JOIN entre os dados do cliente=1 e a tabela de vendas para mostrar as vendas do daquele cliente;

3. Recursos novos

– Adicionado na pasta ./bin/admin o front-end de acesso administrativo para acesso: http://localhost:8080/admin
– Nas instruções PATCH de atualização dos dados no servidor, acrescentado a opção “rowstate”: “modifyORinsert” – tenta fazer um update, caso não encontre nenhum registro então tenta um INSERT;
com isto a propriedade “rowstate” passou a ter as seguintes opções: inserted, updated, modified e modifyORinsert
– proxy para gerar código TypeScript nativo fazendo a chamada: http://localhost:8080/OData/hello/ng irá retornar um código typescript/angular para acesso ao metadata utilizado no servidor;

exemplo: http://localhost:8080/OData/hello/ng
[code]

/// <summary>
/// ODataBr – Generate NG5 Script
/// Date: 17/01/2018 22:39:46
/// Auth: amarildo lacerda – tireideletra.com.br
/// gerado pelo Servidor ODataBr: …/OData/hello/ng
/// </summary>

import { Injectable, Inject } from ‘@angular/core’;
import { HttpClient, HttpHeaders } from ‘@angular/common/http’;
import { ODataProviderService,ODataFactory,ODataService } from ‘./odata-provider.service’;
import { Observable } from ‘rxjs/Rx’;

export interface ODataBrQuery extends ODataService{}

@Injectable()
export class ODataBrProvider {
token:string="";
urlBase:string="";
urlPort:number = 8080;
headers: HttpHeaders;

port(aPort:number):ODataBrProvider{
this.urlPort = aPort;
return this;
}
url(aUrl:string):ODataBrProvider{
this.urlBase = aUrl;
return this;
}

getJson(url:string):Observable<any>{
this.configOptions();
return this._odata.getJson(url);
}
getOData(query:ODataService):ODataProviderService{
this.configOptions();
return this._odata.getValue(query);
}
private configOptions(){
if (this.token!="") { this._odata.token = this.token; };
this._odata.createUrlBase(this.urlBase,this.urlPort);
if (this.headers.keys().length>0) {
//this._odata.headers.clear;
for(let item of this.headers.keys() ){
this._odata.headers.set(item,this.headers.get(item));
}
}
}

constructor(private _odata:ODataProviderService ) {
this.headers = new HttpHeaders();
}
get_filial( query:ODataBrQuery ):ODataProviderService {
this.configOptions();
query.resource = "filial"+(query.join?query.join:"");
return this._odata.getValue( query );
}

get_produtos( query:ODataBrQuery ):ODataProviderService {
this.configOptions();
query.resource = "produtos"+(query.join?query.join:"");
return this._odata.getValue( query );
}

}

[/code]

– Adicionado classe base em Typescript/Angular para acesso o servidor OData com a estrutura fundamental de acesso aos dados
Classe typescript/angular para acesso ao servidor

Exemplo de utilização da classe typescript/angular é possível consulta o font-end de “admin” em: http://front-end admin

ver também:
introdução ao oData

Executar um “select” em paralelo na verdade é bem simples, mas complexo se não deter algumas informações elementares sobre o tratamento que o FireDAC dá ao isolamento de conexão.

Basicamente o processo se dá pelo isolamento da “connection” ao fazer a chamada no banco de dados, talvez aí o maior problema já que é necessário ter uma conexão individual para cada chamada ao banco.

No FireDAC esta disponível na IDE o componente TFDMonitor que é responsável em gerar um novo componente de conexão para cada chamada feita. Este é o caminho mais fácil para utilizar “multithreaded”, assunto que não irei tratar aqui já que o interessante é explorar recursos que entregue mais conteúdo a quem quer entender como o processo acontece.

em construção …. ajude a escrever este texto enviando comentários. Gratidão

Base para o texto:
Unit: Data.FireDAC.Helper
Ver: Exemplo no GIT

Nestes primeiros meses de 2017, o grupo MVCBr dedicou a maior parte do tempo em implementar um servidor OData que permite o acesso a base de dados utilizando protocolo RESTful via HTTP.

FIREBIRD !!!

Sim….. o servidor OData  implementado no MVCBr é um servidor que expõe recursos (resources) armazenados em um servidor FIREBIRD 3.0

Simplificando, o servidor MVCBrServer é um servidor RESTful que utiliza o protocolo OData que suporta o Firebird3.

Arquitetura do Servidor

O servidor é um servidor implementado utilizando componentes INDY para fazer o processamento das requisições. Ao receber a chamada do CLIENTE o motor INDY passa para o framework  “Delphi MVC Framework” implementado pelo “Daniele Teti”. Seguindo os padrões RESTful, o servidor analisa o tipo de pacote que esta recebendo (GET, PUT, POST, PATCH, DELETE, OPTIONS) e passa a requisição para um PARSER OData implementado no framework MVCBr. Ao receber a requisição o PARSER prepara a requisição que será feito ao banco de dados (ex: select * from produtos), avalia o “metadata” contendo as regras de acesso e constrói a solicitação através da estrutura do FireDAC do Delphi, que remete ao driver o FIREBIRD. Recebendo o retorno tudo é empacotado utilizando JSON e devolvido para o cliente.

 

O “metadata” – Onde o modelo relacional é descrito

Junto com o projeto MVCBr irá encontrar um banco FIREBIRD3  (MVCBr.fdb)  que possui uma estrutura básica de teste utilizada no “framework”

Configurar o “databases.conf” do firebird:

#
# Live Databases:
#
mvcbr=[path]/mvcbr.fdb

Como configurar um resource

Resource é o identificar a ser utilizado no HTTP para acessar um determinado recurso do banco de dados.
Veja o exemplo:

    {
      "resource": "produtos",
      "collection": "produtos",
      "keyID": "codigo",
      "maxpagesize": 100,
      "fields": "*",
      "method": "GET,PATCH,POST,PUT,DELETE",
      "relations": [
        {
          "resource": "grupos",
          "sourceKey": "grupo",
          "targetKey": "codigo",
          "join": "left join grupos on (produtos.grupo=grupos.grupo)"
        }
      ]
    }

  • resource:  apelido para o URL utilizada no HTTP
  • collection: nome da tabela fisica no banco de dados
  • keyID: coluna de acesso rápido ás linhas da tabela    ex: http://…./OData/OData.svc/produtos(‘789112313311’)
  • maxpagesize: número máximo de linhas a retornar caso não seja indicado o comando   $top
  • fields: lista de colunas a retornar quando o comando  $select não for indicado
  • method: quais as permissões serão publicadas aos clientes
  • relations: quais relacionamento podem ser executados com “resource” corrente  ( é um DETAIL)
    • relations.resource: qual o apelido do relacionamento com o resource MASTER
    • relations.sourceKey: qual a coluna de recionamento no resource MASTER
    • relations.targetKey: qual a coluna de relacionamento no resource DETAIL
    • relations.join: utilizado para JOINs mais complexos ignorando “sourceKey” e “targetKey”

Listagem completa do metadata de exemplo

[code lang=”javascript”]
{
"@odata.context": "http://localhost:8080/OData/OData.svc",
"__comment": "Services list all resource available to OData.Service",
"OData.Services": [
{
"resource": "produtos",
"collection": "produtos",
"keyID": "codigo",
"maxpagesize": 100,
"fields": "*",
"method": "GET,PATCH,POST,PUT,DELETE",
"relations": [
{
"resource": "grupos",
"sourceKey": "grupo",
"targetKey": "codigo",
"join": "left join grupos on (produtos.grupo=grupos.grupo)"
}
]
},
{
"resource": "grupos",
"collection": "grupos",
"keyID": "codigo",
"fields": "*",
"method": "GET,PATCH,DELETE,PUT,POST",
"maxpagesize": 100,
"relations": [
{
"resource": "produtos",
"sourceKey": "codigo",
"targetKey": "grupo",
"join": "join produtos on (grupos.codigo=produtos.grupo)"
}
]
},
{
"resource": "fornecedores",
"collection": "fornecedores",
"maxpagesize": 100,
"fields": "*",
"keyID": "codigo"
},
{
"resource": "clientes",
"collection": "clientes",
"keyID": "codigo",
"method": "GET,POST,PATCH,UT,DELETE",
"searchFields": "nome",
"maxpagesize": 100,
"fields": "*"
"relations": [
{
"resource": "vendas",
"join": "join vendas on (vendas.cliente=clientes.codigo)"
},
{
"resource": "vendas_item",
"join": "join vendas a on (clientes.codigo=a.cliente) join vendas_item b on (b.documento=a.documento)"
}
]
},
{
"resource": "vendas",
"collection": "vendas",
"maxpagesize": 100,
"keyID": "documento",
"fields": "*",
"method": "GET,POST,PATCH,PUT,DELETE"
},
{
"resource": "vendas_item",
"collection": "vendas_item",
"maxpagesize": 100,
"keyID": "documento"
"method": "GET,POST,PATCH,PUT,DELETE"
}

]

}
[/code]

 

O componente TODataDatasetAdapter é um construtor associado ao Dataset convertendo o JSON enviado pelo servidor em um DATASET navegável no formulário. Vejamos suas propriedades.

TODataDatasetAdapter Onde:
Builder: ligação para o TODataBuilder;
Datasert: ligação para um TFDMemTable (aka. TClientDataset !!!);
ResponseJSON: ligação com o TIdHTTPRestClient;
ResponseType: no momento só aceita texto plano sem compactação – pureJSON;
RootElement:  caminho onde se encontra o ARRAY de valores no JSON enviado pelo servidor (no OData o padrão é “value”);
Introdução ao OData | TODataBuilder | TIdHTTPRestClient

 

O protocolo OData não trata sobre o controle de transação do banco de dados. Esta é na verdade um decisão do server que implementação será feita. Como o OData nasce em um ambiente noSQL com forte presença é fácil imaginar que o controle de transação é uma preocupação mais presente no legado e menos presente nas novas tecnologia.
Aqui um paradigma bom a ser vencido ao longo dos anos – como sair de um ambiente transacional e abandonar o ACID no noSQL…

Aqui no MVCBrServer o banco de transação é o Firebird e a transação faz parte da estrutura de controle. Pensando nisto, quando o cliente solicita um conjunto de linhas em um ARRAY o server faz um STARTTRANSACTION e somente após a conclusão de todas as linhas irá enviar um COMMIT para o servidor.
Caso ocorra algum erro durante o processo um ROLLBACK é enviado para o servidor cancelando todo o lote da transação.

introdução a OData | INSERT | UPDATE

A execução de um UPDATE no banco de dados envolve duas categorias de informações. A primeira delas é uma lista de colunas a atualizar no servidor, na segunda parte de importância é a indicação de quais linhas serão envolvidas na atualização (a WHERE).

Com base na especificação OData para RESTful, o METHOD PUT indica que o cliente deseja fazer uma atualização da tabela. As colunas a serem atualizadas devem ser enviadas no BODY da mensagem ao servidor RESTful. O servidor MVCBrServer esta preparado para receber uma linha simples de colunas/valores, bem como um ARRAY com um conjunto de linhas.

Possibilidades  de formato do BODY para atualizar a(s) linha(s):

  1. Uma linha simples:
      { "id" : "1" ,  "nome":"DESCRICAO TESTE" }
  2. Um ARRAY com uma lista de linhas:
      
          [  { "id": "1",  "nome": "DESCRICAO 1"},
             { "id": "2",  "nome": "DESCRICAO 2"},....
          ] 
    

Se o BODY indica os valores das colunas a serem atualizadas no servidor, próximo passo é tratar a seleção das linhas que iram receber atualização já que em geral queremos atualizar uma linha ou um conjunto delas e raramente desejamos atualizar todas as linhas.
Preferencialmente a definição da WHERE deve ser indicada como parâmetro para o RESOURCE (a tabela ou coleção):

     http://localhost:8080/OData/OData.svc/produtos(id='1')  -> aplica atualiza sobre a linha com ID = "1"
     
     caso o RESOURCE requeira mais de uma coluna - pode-se separa-las por vírgulas

     http://localhost:8080/OData/OData.svc/produtos(id='1',outra='x')  -> aplica atualiza sobre a linha com ID = "1"  AND outra="x"

Caso a coluna não seja indicada, o MVCBrServer irá utilizar a chave indicada no metadata (modelo de dados).

Quando o BODY indica uma lista de linhas em um ARRAY – não é possível indicar a chave nos parâmetros já que cada linha possui uma chave diferente (WHERE). Neste caso o servidor irá utilizar o seu “keyID” do metadata e aplicar o valor de cada linha na WHERE para escolher as linhas que irão receber atualização ou seja, as linhas enviadas no ARRAY devem conter as colunas correspondente a chave do RESOURCE.

Ao caso cabe uma questão – A especificação OData indica que no comando PUT, todas as colunas devem ser enviadas para o servidor. A implementação no MVCBrServer não possui esta exigência – é possível enviar somente as colunas da chave e as colunas a serem atualizadas, ignorando as colunas que não foram enviadas na lista. Nos casos que for necessário enviar um NULL para uma coluna, este desejo deve constar na linha enviada… Ex: {…., “colunaxxx”: NULL, … }

Introdução ao OData | INSERT | DELETE |

O texto anterior mostrei um exemplo de como enviar um INSERT para o servidor usando o METHOD POST padrão RESTful utilizado no MVCBrServer.

Agora vamos olhar como enviar comando DELETE (METHOD DELETE) para o servidor e excluir um linha na tabela.

Há duas possibilidade para excluir registros de uma tabela na estrutura do servidor MVCBrServer. A primeira é enviar uma única linha diretamente pela URI ao servidor indicando o RESOURCE e como parâmetro o identificador padrão da linha – em geral ligado a chave primária:

  • http://localhost:8080/OData/OData.svc/grupos(’08’) – com METHOD DELETE, exclui a(s) linha(s) com chave ’08’ – neste formato o servidor irá utilizar como chave para encontrar a linha a excluir o “keyID” da tabela indicado no metadata – Um cuidado a observar é que neste formato pode ocorrer do metadata se referir a mais de uma linha e neste caso o servidor irá excluir todas as linhas que contenham a mesma informação – exemplo: se for o documento da venda e o RESOURCE for os itens irá excluir todos os itens da venda que contenham o mesmo documento;
  • http://localhost:8080/OData/OData.svc/grupos(grupo=’08’) – neste modelo o servidor irá utilizar a coluna indicada para selecionar as linhas a serem escolhidas o que permite melhor controle pelo “Coder”. Caso seja necessário mais de uma coluna, basta separar por vírgula ex:  …/ItemDaVenda(dcto=’00001′,ordem=1);

Outro formato é o envio no “BODY” com um ARRAY contendo uma lista de linhas a serem excluídas:

  http://localhost:8080/OData/OData.svc/ItemDaVenda

        no BODY enviar o JSONArray:
           [ { "dcto":"00001", "ordem":1},
             { "dcto":"00002", "ordem":1}
              ....
           ]

Introdução ao OData | Fazendo INSERT

Tomando emprestado do padrão RESTful, para enviar um INSERT para o banco de dados com o protocolo OData é preciso formatar uma mensagem com METHOD POST e enviar no BODY da mensagem o JSON correspondente aos dados das colunas a serem inseridas no banco de dados.

 

POST_Cliente

 

No exemplo, o METHOD POST enviou uma linha contendo o registro a persistir no banco de dados. Como a repetição de envios de comandos ao servidor pode se tornar de alto custo, dado ao uso de conectividade de baixa velocidade ou instável – o recomendado é enviar um pacote de dados juntos para que sejam executados no servidor. Para isto, o servidor MVCBrServer prevê a possibilidade de receber um   ARRAY JSON com uma lista de registros a serem inseridos.

Sendo assim, é possível enviar um pacote de dados no seguinte formado:

URL-POST: http://localhost:8080/OData/OData.svc/clientes

[
{"codigo":9999997,
"nome":"Jose da Silva",
"fone":"055-111-12312",
"cidade":"São Paulo"
},

{"codigo":9999998,
"nome":"Jose da Silva8",
"fone":"055-111-12312",
"cidade":"São Paulo"
},

{"codigo":9999999,
"nome":"Jose da Silva9",
"fone":"055-111-12312",
"cidade":"São Paulo"
}
]

Codigo GIT MVCBr

Primeira Parte

FB

 

Introdução
Tomando emprestado o WIKI “plugin” é um componente computacional que adiciona recursos a um programa existente. Quando um programa suporta “plugins” ele permite ser customizado para responder a necessidades não previstas no projeto original.

Uma interface de “plugin” deve prever a possibilidade de um conjunto de código ou janela permitir ser inserida em partes do programa principal.

Em um primeiro instante – é comum encontrar “plugins” que publicam alguma função ou procedimento a ser chamado pelo aplicativo principal. Este modelo limita as funcionalidade dos “plugins” que em geral ficam mais estáticos a serem um item de menu ou uma ou outra funcionalidade.

O “plugin” que irá assinar algum serviço do aplicativo

A idéia que motiva esta publicação é a construção de um modelo de aplicativo principal HOST que publica serviços implementados em seu código e ficam disponíveis para que os “plugins” possam assinar estes serviços.

Então o HOST se torna um “publisher” é o plugin um “subscriber”, onde o plugin que solicita a assinatura de um determinado serviço do HOST.

Com a mecânica em que o “plugin” assina aos serviços do aplicativo servidor permitirá que o “plugin” tome a decisão sobre qual tipo de serviço ele quer assinar no aplicativo principal . Um exemplo é um “plugin” assinar  para ser um item do menu –  em outros casos poderá assinar funcionalidade de uma “aba” de janela ou adicionar “frames” a alguma interface do usuário – entregando mais poder de escolha ao “plugin”.

Segurança x Flexibilidade

Ainda que “flexibilidade” seja o principal objetivo, segurança segue o mesmo caminho da flexibilidade. Há que se questionar o quanto um HOST pode ser seguro o bastante para não permitir um assinante malicioso. Não tenho resposta para a questão, já que um “plugin” maldoso poderia assinar a serviços para aplicar táticas maliciosas – questão que precisa ser avaliada.

diagrama

Uma interface para publicar serviços

Considerando que o programa principal tem um formulário:

TMeuMenu = class(TForm)

end;

o que precisamos fazer é dotar o formulário principal de uma interface que permita publicar os seus serviços:

[code lang=”pascal”]
IPluginApplication = interface
[‘{6ED989EA-E8B5-4435-A0BC-33685CFE7EEB}’]
procedure RegisterMenuItem(const AParentMenuItemName, ACaption: string; ADoExecute: IPluginMenuItem);
procedure RegisterToolbarItem(const AParentItemName, ACaption: string; ADoExecute: IPluginToolbarItem);
procedure RegisterAttributeControl(const AType,ASubType: Int64; ADoExecute: IPluginControl);
end;
[/code]

Com isto o formulário passa a implementar a interface de serviço de “plugins” da seguinte forma:

TMeuMenu = class(TForm, IPluginApplication)
….
end;

 

Registrando o Plugin para o Aplicativo Principal

Antes de carregar um “plugin” o aplicativo host precisa conhece-lo (uma lista de plugins registrados). Uma forma simplista, é guardar uma lista de “plugins” disponíveis em um arquivo INI do host. A lista é necessária para que o aplicativo possa fazer carga dos seus “plugins”. Uma vez feito a carga do plugin, estes  irão assinar os serviços a que desejam interagir com o host.

O framework utiliza a implementação de DLLs para troca de código com o aplicativo principal, publicando duas chamadas – uma para carga inicial da DLL e outra para quando o aplicativo principal esta encerrando o uso do plugin:

[code]
function LoadPlugin(AAplication: IPluginApplication): IPluginItems;
procedure UnloadPlugin;

exports LoadPlugin, UnloadPlugin;
[/code]

Carregando o Plugin
Sabedor destas duas entradas disponíveis no “plugin”, resta escrever os métodos de carga do plugin (ver TPluginManager nos fontes):

[code lang=”pascal”]
function LoadPluginService(APlugin: string;
AAppliction: IPluginApplication): Integer;
var
F: function(APApplication: IPluginApplication): IPluginItems;
H: THandle;
begin
result := -1;
H := LoadLibrary(PWideChar(APlugin));
if H &gt; 0 then
begin
try
@F := GetProcAddress(H, ‘LoadPlugin’);
if assigned(F) then
result := LPluginManager.FPlugins.Add(H, F(AAppliction))
else
raise Exception.Create(‘Não carregou o plugin’);
except
FreeLibrary(H);
end;
end;
end;
[/code]

Note que a função “LoadPlugin” do plugin espera que seja enviado um IPluginApplication e retorna uma lista de plugins existente na mesma DLL, já que uma DLL pode conter 1 ou mais plugins.

O IPluginApplication, representa a interface do HOST que publica os serviços que os plugins poderão assinar no HOST – Internamente a DLL irá montar uma lista de plugins disponíveis. Cada plugin da DLL se encarrega de assinar os serviços do HOST.

Estendendo os serviços do HOST

A interface que publica os serviços no HOST é o IPluginApplication que já possui três serviços básicos para a assinatura sendo eles:

  1. MenuItem – Assinatura para se registrar em um item de menu do HOST;
  2. ToolbarItem – Assinatura para se registrar em um item da Toolbar;
  3. AttributeControl – Uma assinatura a utilizar o plugin como um “control” ou atributo de um janela.

Caso deseje implementar outros serviços para o HOST, estendendo suas funcionalidades é possível estender a interface IPluginApplication e implementa-la no HOST:

[code]
IMyPluginApplication  =  interface(IPluginAppliction)
procedure RegisterXXX(….);
end;

// no host
TMyMainMenu = class(TForm, IMyPluginApplication )

end;
[/code]

 

Parte 2 – Construindo o Plugin

(No final o código estará disponível no GIT)
….