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. Confira o artigo!
Introdução
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:
1 |
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?
Dá-lhe RTTI!
Para apresentar estes dados, faremos o RTTI entrar em cena!
Considere a existência de uma classe de funcionários com os seguintes atributos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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, ok?
Considere também que a nossa View (neste caso, um formulário) recebe uma lista de objetos do tipo TFuncionario
:
1 |
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.
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 DataSet, no qual possui apenas um Field chamado “Nome”, adicionado manualmente pelo Fields Editor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 classeTFuncionario
; - Invocamos o método
GetProperty
para obter a propriedade “Nome” comoTRttiProperty
; - 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:
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 por conta própria. Pensei, então, em renomear cada componente visual da tela com o nome da respectiva propriedade, precedido do prefixo “Campo”. Este foi o resultado:
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
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 DataSet, chamando o método acima:
1 2 3 4 |
procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet); begin PreencherCampos(FListaFuncionarios[Pred(ClientDataSet1.RecNo)]); end; |
Por que o “Pred”?
A propriedade 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:
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!
Bom dia André, Primeiramente parabéns pelo artigo, uma pequena dúvida sobre campos blob, como trabalhar usando ele na RTTI ?
Att
Bom dia, Júnior, como vai?
Não entendi muito bem o seu questionamento. Vou entrar em contato por e-mail.
Abraço!
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.
Olá, Marcos!
Ótima pergunta! Vou fazer alguns testes e respondo no seu e-mail.
Abraço!
Bom dia, André… Excelente artigo, já me ajudou muito, mas desejo fazer uma pergunta: seria possível fazer a operação inversa, ou seja: vincular um componente (TEdit, por exemplo) a uma propriedade de um objeto de negócio, de tal maneira que ao se alterar o valor do componente essa alteração se reflita no valor da propriedade?
Excelente pergunta, Francisco!
Enquanto eu escrevia o artigo, pensei a mesma coisa. Fiz uma tentativa de modelar um objeto de negócio que pudesse ser conectado aos controles visuais através do recurso de LiveBindings, mas sem sucesso. Talvez, essa operação possa ser possível se criarmos o objeto de negócio herdado de algum container (como
TClientDataSet
) ou de outra classe específica para habilitar o binding em tempo real, porém, não me aprofundei nos testes.Obrigado! Abraço!
Caro amigo, uma pergunta talvez idiota…
Como populou a classe TFuncionarios com os dados do Banco? Em que momento fez isso? Parece que seu exemplo ja pressupoe que a Classe TFuncionarios continha os dados persistidos. Mas eu na verdade, estou querendo saber como popular as propriedades de uma Classe com os dados do Banco, porque so depois disso poderei preencher a lista e posteriormente chama-la, nao é mesmo?
Obrigado!
Olá, Wilton, tudo bem?
Ótima questão. No exemplo do artigo, os dados dos funcionários são adicionados temporariamente na criação do formulário, apenas para fins didáticos.
Em uma aplicação em produção, a lista de objetos teria de ser preenchida com os dados da tabela de funcionários, como no código abaixo:
Abraço!
Muito bom meu amigo, essa era a resposta que eu estava esperando. Mas em Rtti, como faria essa busca no Banco? Poderia colocar um modelo completo para n’os em algum artigo?
Olá, Wilton!
A consulta no banco pode ser realizada da forma tradicional, utilizando os componentes de conexão do próprio Delphi, como ADO, IBX, DBX, FireDAC ou tecnologias de terceiros. A parte do RTTI entra em ação somente quando os dados já estão consultados.
Abraço!
Olá André inicialmente, parabéns pelo artigo, ótimo conteúdo muito bem explicado, mas ainda fiquei numa dúvida básica, se sua FListaFuncionarios tivesse 5.000 registros iria consumir um pouco de memória e iria acumulando mais memória conforme fosse a chamando, como você faz para limpar de forma a não acumular o uso dessa memória ?
Olá, Eduardo. Muito obrigado pelo feedback!.
Para ser sincero, também tenho essa dúvida. Antes de respondê-lo, vou estudar um pouco mais sobre este assunto e, provavelmente, elaborar um artigo para desmistificar este contexto de uso de memória com lista de objetos.
Abraço!
Olá André tudo certo?
Olhando o seu post, me lembrou muito o que o livebindings faz (debaixo dos panos).
Passei um tempo me batendo, mas para exibir um objeto em tela sem usar um DataSet como o ClientDataset ou o FdMemTable, utilizei este método:
No form utilizo os componentes TAdapterBindSource e TDataGeneratorAdapter.
Assim não preciso de um clientdataset. Na prática é quase a mesma coisa.
Att.
Olá, Lucas, tudo bem?
Desculpe-me pela demora. Por algum motivo, o blog moveu o seu comentário para a caixa de spams, mas consegui recuperá-lo.
Lucas, esse código que você apresentou é exatamente o que eu tentei fazer por muito tempo!
Vou entrar em contato para aprender com você. 🙂
Obrigado!
Grande André, mais uma vez fui direcionado ao seu site. Parabéns pelo artigo.
Como eu deveria proceder para persistir esses valores em banco após a edição ou inserção?
Olá, Roberto! Ótima pergunta!
Para trabalhar com inserção ou alteração de dados, eu recomendaria a declaração de um método na classe
TFuncionario
responsável por persistir os seus próprios valores (propriedades) no banco de dados. Para isso, uma nova classe de persistência teria que ser criada para essa finalidade (conectar-se ao banco de dados e enviar os valores). Na prática, este método ficaria da forma abaixo, considerando que “DAO” é a classe de persistência:Grande abraço!
Boa noite André, tudo blz?
Gostaria de saber, se você pode me “salvar” (mais uma vez, kkkk…), com algum exemplo utilizando objetos compostos?
No meu caso, estou tentando “ler” os dados um objeto do tipo TCliente, mas, o problema está em ler os dados da propriedade Telefone (do tipo TTelefone):
Desde já agradeço pela sua atenção.
Olá, Leonardo! Ótima questão.
Vou entrar em contato com você para entender melhor a sua estrutura de classes.
Abraço!
Boa noite André,
Primeiramente, obrigado pelo retorno e parabéns pelo trabalho fantástico que você faz aqui no blog…
Depois de tanto quebrar a cabeça, consegui resolver e por isso estou postando o código aqui, para quem quiser dar uma olhada.
O código em si, trata o seguinte cenário, eu tenho um objeto TCliente (classe), e esse objeto é composto por outro objeto TTelefone (classe), além de outras propriedades, e eu utilizei os recursos da RTTI para ler essas propriedades e gerar um comando SQL (Insert).
#DECLARAÇÕES
#METODO QUE GERA O COMANDO SQL
Grande abraço.
Olá, Leonardo!
Em primeiro lugar, gostaria de parabenizá-lo por elaborar essa solução. Ficou muito interessante! O RTTI é um recurso bem poderoso para essas funcionalidades.
Em segundo lugar, obrigado pela contribuição! O seu código provavelmente ajudará outros desenvolvedores.
Grande abraço!
Ótimo Post André Celestino. Teria como listar em um DBGrid uma TList? vi algo parecido no site do Marco Cantu, mas não era possível adicionar objetos. Seria possível criar uma classe TDataset onde eu pudesse utilizar o dataset para manipular uma lista?
Olá, Francisco!
O componente
TDBGrid
exige uma conexão com umTDataSource
(e este com umTDataSet
) para que os dados sejam exibidos.No caso de listas de objetos, recomenda-se trabalhar com o componente
TStringGrid
. No artigo sobre Visitor eu apresento uma forma de implementar essa funcionalidade com o componenteTAdapterBindSource
.Abraço!
Olá André, mais um excelente artigo.
Eu estou tentando fazer uma coisa aqui, mas sem sucesso por enquanto. É o seguinte: Eu tenho um form e dentro dele um TEdit no qual eu gostaria de “conversar com ele”. Eu passo o form como Context e faço um loop rodando Field por Field para achá-lo (ele tem um CustomAttribute) e até aí tá tudo bem. Depois que eu achei ele no loop eu quero pegar o valor na propriedade Text dele, eu até localizo a propriedade Text, só que eu não tenho a referência dele para passar no Property.GetValue. Existe alguma propriedade onde eu consigo capturar a referência dentro de um RttiField?
Segue código de exemplo:
Olá, Ricardo, boa tarde!
Excelente pergunta! Até tive que abrir o Delphi para fazer alguns testes, rsrs.
Ricardo, eu acredito que pode ser feito da forma abaixo, obtendo a instância do componente da tela:
Faça o teste e me avise!
Bom dia. Interessante artigo, estava procurando algum artigo para entender como funciona o RTTI, parabéns.
Desculpa pelo fato de não conhecer o GitHub mas quando cliquei no link ele abre mas não sei como fazer para baixar o projeto exemplo, será que poderia me orientar?
Olá, Laerdes!
Em primeiro lugar, obrigado pelo feedback!
Para baixar o projeto, entre nesse link:
https://github.com/AndreLuisCelestino/Exemplos-Blog
Em seguida, clique no botão “Clone or download” na parte superior à direita e selecione “Download ZIP”.
Abraço!
Ola André!
Parabéns pelo artigo, é algo que ajuda bastante.
Tenho uma duvida: é possível eu fazer um processo que eu criei atributos de uma classe em run-time, exemplo tenho uma classe:
E em determinado processo do sistema eu precise criar um 3º atributo
FCampoC: string;
é possível?
Olá, Messias! Excelente pergunta! Eu tive essa mesma dúvida há alguns meses.
Pelo que estudei, infelizmente não é possível alterar a estrutura de uma classe em runtime. Nesse caso, uma alternativa é trabalhar com uma lista de chave-valor dentro da classe, de forma que você possa adicionar e remover valores a qualquer momento.
Por exemplo:
Com Generics, você pode tirar proveito de bons recursos, como Iterators, Enumerators, List e Sort.
Abraço!
Boa tarde André, muito inspirador e salvador ao mesmo tempo, já agradeço essa luz.
estou com um caminho sem saída até agora, falta um pouco de conhecimento…
Eu tenho um Record de “transferência”, nesse record ele possui um Ponteiro, Objeto e um Array de string
Ponteiro := TList.Classinfo;
Array := [‘Campo1’, ‘Campo7’, ‘Campo8′]
Com essa área de transferência quero montar um form de retorno de pesquisa genérico onde ele vai receber o TList e mostrar os campos do Array de strings.
A duvida é como faço no RTTI para saber o qual classe é o do List e fazer a tipagem?
TClass(Tlist).’CAMPO1′, TClass(Tlist).’CAMPO7′, TClass(Tlist).’CAMPO8’
Olá, Marcelo, tudo bem?
Não consegui compreender muito bem a sua dúvida.
Mesmo assim, normalmente é possível identificar o tipo do objeto através da classe TRttiType. Com ela, portanto, você pode validar se o objeto é da classe TList.
Caso eu não tenha respondido a sua dúvida, envie mais detalhes para [email protected]. Se possível, envie um pequeno código de exemplo de como você vai utilizar esse record.
Abraço!
Bom dia, André outra duvida eu vi ontem a Delphi Conf 2023 USA e o Agnes se não me engano disse que é melhor fazer assim:
TEdit(Componente).Text := Valor
Do que assim:
(Componente as TEdit).Text := Valor
Pois evitamos Cast desnecessário visto que já tem um if Componente is TEdit then garantindo que é um TEdit.
Me parece fazer bastante sentido.
Grande Abraços e parabéns.
Olá, Ronaldo.
No meu entendimento, as duas formas são typecastings, então não faz diferença. Na verdade, o operador “as” executa uma pré-validação para verificar se o typecasting é válido. Veja uma resposta sobre isso nesse link do StackOverflow. No entanto, ele tem razão em um ponto: se há um IF para garatir que o componente é um TEdit, então você realmente pode utilizar um cast direto (chamado de hard cast), convertendo diretamente para TEdit, já que estará seguro de que não receberá um Access Violation.
Eu, particularmente, utilizo o operador “as” por mais segurança, mas, claro, não é uma regra.
Abração!
Boa noite, Andre e pessoal.
Estou muitas dificuldades de entender a RTTI, eu preciso fazer o seguinte pegar tudo que está no formulário nos campos edits, combos eu criei uma classe TBind = class(TCustomAttribute) que consegui através do curso do Thulio fazer o DataSet popular a tela, só que agora preciso pegar os campos da tela que estão com a marcação do exemplo abaixo “TBind” e invocar uma Classe (TCliente) e popula-lá via RTTI com esses dados do Formulário e não faço a menor ideia de como fazer, se alguém puder me ajudar eu agradeço, pois senão terei que popular e criar a classe na “unha”, campo a campo.
[TBind(‘A9_ID’, ”,true)]
edtCodigoInterno: TEdit;
TCliente = model
Public
Codigo: integer;
Olá, Ronaldo, tudo bem?
Vou entrar em contato com você por e-mail.
Bom dia Andre,
Estou utilizando seu exemplo para alimentar uma FDMemTable porém na linha: “…FDMemTable.AppendRecord([Proprieadeidtbcliente.GetValue(Cliente).AsInteger]);…” ocorre um erro de “invalid class type cast”.
Você poderia ajudar com essa situação?
Olá, Wellinton.
Desculpe pela demora. Vou entrar em contato com você.