[Delphi] Usando RTTI para exibir dados de um objeto de negócio em componentes visuais

[Delphi] Usando RTTI para exibir dados de um objeto de negócio em componentes visuais

Saudações, amigos!
Vejam só o RTTI entrando novamente na pauta de artigos! Tive uma boa recepção com o artigo sobre validações com RTTI e decidi abordar um pouco mais sobre este recurso.
Dessa vez, utilizaremos o RTTI para preencher os campos de um formulário de acordo com os valores das propriedades de um objeto de negócio. Ficou confuso? Confira o artigo!

 

Quando trabalhamos com algum padrão de arquitetura, como MVC (Model-View-Controller) ou MVP (Model-View-Presenter), existe uma camada de modelagem (Model) que comporta as classes de negócio, geralmente mapeadas conforme as tabelas do banco de dados. A criação dessas classes é resultado de uma técnica de desenvolvimento conhecida como ORM (Object-relational Mapping), que visa simplificar as operações de persistência através do mapeamento das tabelas.
Há várias formas de apresentar os dados recuperados de uma consulta no banco de dados na camada de visão (View). Uma delas é trazer uma lista de objetos, considerando que cada objeto representa um registro retornado na consulta. Para facilitar esse entendimento, considere, por exemplo, que existam três funcionários cadastrados da cidade de Maringá. Para trazê-los, executaríamos a consulta abaixo:

SELECT * FROM FUNCIONARIOS WHERE Cidade = 'Maringá'

O resultado dessa consulta é uma lista de objetos com três instâncias da classe de funcionários (como TFuncionario). Pois bem, ao receber essa lista de objetos na camada View, devemos exibir os valores do funcionário selecionado nos controles de tela, porém, a lista não possui uma propriedade para conexão com um DataSource.

 

E agora?
Faremos o RTTI entrar em cena!
Para demonstrar o exemplo, considere a existência de uma classe de funcionários com os seguintes atributos:

uses
  Vcl.Graphics;
 
type
  TFuncionario = class
  private
    FCodigo: integer;
    FNome: string;
    FEstadoCivil: string;
    FSexo: string;
    FSenioridade: integer;
    FDataNascimento: TDateTime;
    FCorUniforme: TColor;
    FPlanoSaude: boolean;
  public
    property Codigo: integer read FCodigo write FCodigo;
    property Nome: string read FNome write FNome;
    property EstadoCivil: string read FEstadoCivil write FEstadoCivil;
    property Sexo: string read FSexo write FSexo;
    property Senioridade: integer read FSenioridade write FSenioridade;
    property DataNascimento: TDateTime read FDataNascimento write FDataNascimento;
    property CorUniforme: TColor read FCorUniforme write FCorUniforme;
    property PlanoSaude: boolean read FPlanoSaude write FPlanoSaude;
  end;

Dado que o exemplo é didático, não me preocupei com o encapsulamento de Getters e Setters.
Considere também que a nossa View (neste caso, um formulário) recebe uma lista de objetos do tipo TFuncionario:

FListaFuncionarios: TObjectList<TFuncionario>;

A nossa missão, com RTTI, é codificar a exibição dos dados na tela de exemplo abaixo, sem trabalhar com a propriedade Data do TClientDataSet e também dispensando o recurso de LiveBindings.

Tela de exemplo para preenchimento dos componentes com RTTI

 

Vamos lá!
Observe que há somente a coluna “Nome” no componente TDBGrid. A ideia é que, ao clicar no nome do funcionário, os dados sejam exibidos no painel à direita. Primeiro, portanto, codificaremos uma rotina para preencher o nosso ClientDataSet, no qual possui apenas um Field chamado “Nome”, adicionado manualmente pelo Fields Editor.

procedure TForm1.PreencherDataSet;
var
  Contexto: TRttiContext;
  Tipo: TRttiType;
  PropriedadeNome: TRttiProperty;
  Funcionario: TFuncionario;
begin
  // Cria o contexto do RTTI
  Contexto := TRttiContext.Create;
  try
    // Obtém as informações de RTTI da classe TFuncionario
    Tipo := Contexto.GetType(TFuncionario.ClassInfo);
 
    // Obtém um objeto referente à propriedade "Nome" da classe TFuncionario
    PropriedadeNome := Tipo.GetProperty('Nome');
 
    // Percorre a lista de objetos, inserindo o valor da propriedade "Nome" do ClientDataSet
    for Funcionario in FListaFuncionarios do
      ClientDataSet1.AppendRecord([PropriedadeNome.GetValue(Funcionario).AsString]);
 
    ClientDataSet1.First;
  finally
    Contexto.Free;
  end;
end;

Fácil, não? Mesmo assim, vale a explicação:

  • Criamos um objeto do tipo TRttiContext para acessar os dados de estruturas de classes;
  • Usamos um objeto do tipo TRttiType para ler a estrutura da classe TFuncionario;
  • Invocamos o método GetProperty para obter a propriedade “Nome” como TRttiProperty;
  • Dentro do for-in, extraímos o valor da propriedade “Nome” de cada objeto através do método GetValue.

Ao executar o método acima, a nossa Grid já apresenta os dados:

DBGrid preenchida com dados capturados via RTTI

 

O próximo passo é exibir os dados do funcionário selecionado na Grid, percorrendo as propriedades do objeto atual da lista. No entanto, mais uma vez, não existe uma forma visual de associar as propriedades do objeto com os controles da tela, portanto, devemos criar algum tipo de associação. Pensei, então, em renomear cada componente visual da tela com o nome da respectiva propriedade, precedido do prefixo “Campo”. Este foi o resultado:

Nomes dos componentes para associar com propriedades do objeto

 

Feito isso, codificaremos um método que percorrerá as propriedades do objeto e, para cada uma, encontrará o componente na tela (com base no prefixo “Campo”) para preencher o valor.
Porém, observe que há vários tipos de componentes na tela: TEdit, TComboBox, TRadioGroup, TDateTimePicker, TTrackbar, TShape e TCheckBox. Como a atribuição de valor para cada um deles é feita de forma diferente (propriedades Text, ItemIndex, Position, Date e Checked), é necessário testar se o componente encontrado é de uma classe específica e, em caso positivo, converter o componente para prosseguir com a atribuição. Para isso, utilizaremos os operadores is e as do Delphi para trabalhar com conversão segura, principalmente para evitar as exceções de Invalid Typecast.
Chega de conversa e vamos à codificação!

procedure TForm1.PreencherCampos(Funcionario: TFuncionario);
var
  Contexto: TRttiContext;
  Tipo: TRttiType;
  Propriedade: TRttiProperty;
  Valor: variant;
  Componente: TComponent;
begin
  // Cria o contexto do RTTI
  Contexto := TRttiContext.Create;
 
  // Obtém as informações de RTTI da classe TFuncionario
  Tipo := Contexto.GetType(TFuncionario.ClassInfo);
 
  try
    // Faz uma iteração nas propriedades do objeto
    for Propriedade in Tipo.GetProperties do
    begin
      // Obtém o valor da propriedade
      Valor := Propriedade.GetValue(Funcionario).AsVariant;
 
      // Encontra o componente relacionado, como, por exemplo, "CampoNome"
      Componente := FindComponent('Campo' + Propriedade.Name);
 
      // Testa se o componente é da classe "TEdit" para acessar a propriedade "Text"
      if Componente is TEdit then
        (Componente as TEdit).Text := Valor;
 
      // Testa se o componente é da classe "TComboBox" para acessar a propriedade "ItemIndex"
      if Componente is TComboBox then
        (Componente as TComboBox).ItemIndex := (Componente as TComboBox).Items.IndexOf(Valor);
 
      // Testa se o componente é da classe "TRadioGroup" para acessar a propriedade "ItemIndex"
      if Componente is TRadioGroup then
        (Componente as TRadioGroup).ItemIndex := (Componente as TRadioGroup).Items.IndexOf(Valor);
 
      // Testa se o componente é da classe "TCheckBox" para acessar a propriedade "Checked"
      if Componente is TCheckBox then
        (Componente as TCheckBox).Checked := Valor;
 
      // Testa se o componente é da classe "TTrackBar" para acessar a propriedade "Position"
      if Componente is TTrackBar then
        (Componente as TTrackBar).Position := Valor;
 
      // Testa se o componente é da classe "TDateTimePicker" para acessar a propriedade "Date"
      if Componente is TDateTimePicker then
        (Componente as TDateTimePicker).Date := Valor;
 
      // Testa se o componente é da classe "TShape" para acessar a propriedade "Brush.Color"
      if Componente is TShape then
        (Componente as TShape).Brush.Color := Valor;
    end;
  finally
    Contexto.Free;
  end;
end;

Bom, acredito que os comentários no código são autoexplicativos, não é? 🙂
Você deve notado que este método recebe um objeto do tipo TFuncionario como parâmetro. Este objeto está contido na lista de objetos (FListaFuncionarios) e devemos acessá-lo pelo índice do registro selecionado na Grid. Faremos isso através do evento AfterScroll do nosso ClientDataSet, chamando o método acima:

procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
  PreencherCampos(FListaFuncionarios[Pred(ClientDataSet1.RecNo)]);
end;

Por que o “Pred”?
O RecNo inicia no número 1, enquanto os índices de lista iniciam no 0, então é preciso decrementar o valor.
Pessoal, aproveitando o ensejo, é importante destacar que utilizo o RecNo aqui apenas para fins didáticos. Em um ambiente real, em que é possível adicionar, editar e remover registros, eu sugiro que o mecanismo de acesso ao índice da lista seja mais confiável.

 

Por fim, vejam só o resultado final:

Controles visuais preenchidos através de RTTI

 

Caso queiram baixar este projeto de exemplo, acesse o link abaixo do meu GitHub:

https://github.com/AndreLuisCelestino/Exemplos-Blog/tree/master/PreenchimentoComRTTI

 

Fico por aqui com mais essa colaboração sobre RTTI.
Um grande abraço!


Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

4 comentários

  1. Bom dia André, Primeiramente parabéns pelo artigo, uma pequena dúvida sobre campos blob, como trabalhar usando ele na RTTI ?

    Att

  2. Olá André,

    Gostei muito do artigo, está me ajudando em uma demanda aqui na minha empresa.
    Tenho uma pergunta para você. é possível verificar se todas as propriedades de um objeto funcionário possuem o mesmo valor de algum outro da lista de objetos sem a necessidade de um loop em todas as propriedades?

    Att.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.