Article Details                  
 
Programação em Asp.Net - parte IV

Apresenta três novos controlos de acesso a fonte de dados

Programação em Asp.Net - parte IV


Autor: Luís Abreu
Conteúdo: Introdução à programação em Asp.Net 2.0
Ferramentas: Visual Web Dev Express


Na sequência do último artigo, vamos continuar a falar acerca dos novos controlos de acesso a dados. Hoje vamos falar dos três controlos que faltam: DataSetSourceControl, ObjectDataSource e SiteMapDataSource.


Antes de prosseguir, um agradecimento muito especial ao Paulo Morgado (MVP e participante frequente nas listas de discussão .net portuguesas) pelo tempo que disponibilizou para efectuar os testes necessários à utilização do controlo DataSetDataSource.


SiteMapDataSource


Este controlo é utilizado para obter os dados relativos à navegação num site. Uma vez que dediquei o primeiro artigo desta série ao novo esquema de navegação num site, vou redireccionar o leitor interessado em aprender mais sobre este controlo para esse artigo.


DataSetDataSource


Tal como acontece com o XmlDataSource, este controlo permite-nos trabalhar com dados hierárquicos definidos através de xml. A principal diferença em relação ao controlo XmlDataSource prende-se com o facto deste controlo ser adequado a dados XML flat. Tal como acontece com o seu congénere (XmlDataSource), este controlo permite-nos transformar os dados obtidos inicialmente (através da propriedade TransformFile ou do elemento Transform), definir os elementos mostrados (através da propriedade XPath), etc.


Para além disso, podemos ainda obter uma referência ao DataSet (interno) através do método GetDataSet. Tal como acontece com o XmlDataSource, também é possível actualizarmos os dados contidos neste controlo, desde que se respeitem as seguintes regras:



  • a propriedade ReadOnly tem de estar a false;

  • O XML tem de ser obtido a partir de um ficheiro (por outras palavras, temos de utilizar a propriedade DataFile e não o elemento Data);

  • Temos de definir e associar um schema ao controlo. Neste schema devemos definir (pelo menos) um campo chave.

Para demonstrar estas potencialidades, vamos utilizar um exemplo prático. Vamos supor que temos o seguinte ficheiro de XML :

<?xml version="1.0" encoding="utf-8"?>
<class>
<student>
<id>a1</id>
<nome>Luis</nome>
</student>
<student>
<id>a2</id>
<nome>Rui</nome>
</student>
<student>
<id>a3</id>
<nome>Rita</nome>
</student>
</class>

Este XML é validado pelo seguinte schema:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="class">
<xs:complexType>
<xs:sequence>
<xs:element name="student" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:string" />
<xs:element name="nome" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:key name="classKey1">
<xs:selector xpath="student" />
<xs:field xpath="id" />
</xs:key>
</xs:element>
</xs:schema>

Se necessitarmos e efectuar modificação dos dados temos de obrigatoriamente definir uma chave. Para tal devemos utilizar o elemento key no nosso schema (como o código anterior demonstra). Devido ao controlo DataSetDataSource, a obtenção destes dados pode ser feita de forma muito simples. O excerto seguinte demonstra os passos necessários à edição dos dados através de uma gridview:

<asp:GridView ID="GridView1" Runat="server" DataSourceID="DataSetDataSource1"
BorderWidth="1px" BackColor="White" CellPadding="3" BorderStyle="None" BorderColor="#CCCCCC">
<FooterStyle ForeColor="#000066" BackColor="White"></FooterStyle>
<PagerStyle ForeColor="#000066" HorizontalAlign="Left" BackColor="White"></PagerStyle>
<HeaderStyle ForeColor="White" Font-Bold="True" BackColor="#006699"></HeaderStyle>
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" ShowSelectButton="True"></asp:CommandField>
</Columns>
<SelectedRowStyle ForeColor="White" Font-Bold="True" BackColor="#669999"></SelectedRowStyle>
<RowStyle ForeColor="#000066"></RowStyle>
</asp:GridView>&nbsp;
<asp:DataSetDataSource ID="DataSetDataSource1" Runat="server" ReadOnly="false" DataFile="~/class.xml"
SchemaFile="~/class.xsd">
</asp:DataSetDataSource>

Como podemos ver, não existe praticamente nenhuma diferença entre este código e os exemplos apresentados no artigo anterior. Portanto, nada de muito complicado. Queria voltar a salientar a importância de definir uma chave a nível do schema; se tal não acontecer, irá ser gerada uma excepção quando tentarmos modificar os dados relativos a um registo.


Nota: este sample não funciona no Visual Web Developer Express 2005 beta 1. Contudo, funciona quando utilizado num sistema em que é instalado o Visual Studio 2005. Apesar de todos os meus esforços, não consegui obter uma resposta em relação a este problema. Tal deveu-se essencialmente a dois aspectos: em primeiro lugar, a maioria dos utilizadores possui o Visual Studio 2005; em segundo lugar, o controlo deverá ser removido da próxima versão beta, pelo que não deverá constar na versão final da nova framework.


ObjectDataSource


Até agora temos vindo a utilizar controlos que, apesar de muito interessantes, são apropriados para arquitecturas de duas camadas. Este tipo de solução permite o desenvolvimento rápido de aplicações, mas torna-se pouco adequado quando pretendemos construir uma solução escalável. Hoje em dia é costume desenvolver-se souções baseadas em várias camadas. Nestes casos, o acesso à base de dados costuma ser encapsulado por um conjunto de objectos. O meu método preferido (a nível de arquitectura de uma solução OO distribuída) consiste em contruir um modelo de objectos caracterizado pelas seguintes funcionalidades:



  • construção de wrappers que apenas encapsulam os campos contidos nas tabelas; para além de encapsular os campos das tabelas, é normal estas classes conterem código que lhes permite costumizar a serialização (XML ou binária);

  • construção de colecções de elementos anteriores;

  • em algumas situações, os relacionamentos de um-para-n são representados através de propriedades especiais do tipo colecção;

  • utilização de um terceiro tipo de objectos que apenas efectua as operações de inserção, alteração, eliminação e carregamento dos dados a partir da base de dados.

A escolha de uma arquitectura deste tipo não é adequada a todas as situações; contudo, adequa-se (razoavelmente) bem a arquitecturas distribuídas devido à facilidade de adaptação a vários cenários (por exemplo, podemos tornar os objectos de acesso às base de dados em objectos remotos - basta derivá-los de MarshalByRef).


Voltando ao que importa (e, que neste caso, é a utilização do controlo ObjectDataSource), nenhum dos controlos de acesso a fonte de dados apresentados anteriormente é indicado para estas situações. Felizmente para nós, a Microsoft forneceu-nos o controlo ObjectDataSource. Devido à proliferação deste tipo de arquitecturas, este controlo pode vir-se a tornar num dos controlos mais importantes fornecidos pela nova framework (isto apesar de algumas limitações/defeitos que encontrei ao longo dos testes que efectuei com este controlo). Segundo a documentação, existem algumas restrições que devem de ser tidas em conta aquando da utilização deste tipo de controlos:



  • não podem possuir estado;

  • Têm de possuir métodos que são directamente mapeados nas operações de select, insert, update e delete;

Neste tipo de objectos os parâmetros contidos nos elementos XXXParameters são directamente mapeados em eventuais parâmetros definidos pela assinatura da função. Vamos começar com um exemplo simples. Vamos supor que temos uma base de dados que obedece ao seguinte esquema:



Atendendo aos principios enunciados anteriormente, optamos por construir um modelo de objectos que irá encapsular o acesso a esta nossa aplicação. O seguinte diagrama de UML apresenta o nosso modelo de objectos:



Bem, agora que já temos um exemplo simples, podemos começar por construir uma página simples. Não vou estar a desperdiçar o tempo do leitor com uma repetição dos exemplos apresentados na MSDN. Vou, isso sim, tentar apresentar alguns aspectos não demonstrados nos poucos excertos de código apresentados pela MSDN. Quando utilizamos um controlo deste tipo temos de fornecer um construtor pode defeito, que é instanciado automaticamente pelo nosso controlo. Para além disso, também é recomendado construirmos uma classe sem estado.


Este tipo de restrição torna dificil a utilização da minha hierarquia de objectos devido ao facto de eu ter configurado a minha classe de forma a que esta contenha um construtor que inicializa uma variável interna com a connection string de ligação à base de dados. Ao contrário dos exemplos surrealistas da documentação (em que os objectos de acesso a dados utilizam a classe ConfigurationSettings para obterem a connection string a partir do web.config), eu prefiro construir um modelo de objectos mais generalista de forma a que este possa ser acedido a partir de qualquer tipo de aplicação. Portanto o nosso primeiro obstáculo consiste em personalizar a criação do objecto de acesso a dados de forma a que este evoque o construtor correcto.


A resolução deste problema revelou-se extremamente simples uma vez que a classe ObjectDataSource possui um evento ( ObjectCreating) que nos permite resolver este tipo de problemas. Assim, podemos processar o evento como o método seguinte demonstra:

 
private void OnObjectCreating( object sender, ObjectDataSourceEventArgs args )
{
args.ObjectInstance = new FuncionariosCollectionDB( ConfigurationSettings.ConnectionStrings["Funcionarios"].ConnectionString );
}

Esta é a única forma que temos de influenciar a construção do nosso objecto de acesso a dados. Bem, agora que já contornámos a nossa primeira dificuldade, já podemos (pelo menos) visualizar os dados numa grid:

<asp:GridView ID="GridView1" Runat="server" DataSourceID="ObjectDataSource1" AutoGenerateColumns="False"
BorderWidth="1px" BackColor="LightGoldenrodYellow" GridLines="None" CellPadding="2"
BorderColor="Tan" ForeColor="Black" DataKeyNames="IdFuncionarios" AllowSorting="true" AllowPaging="true" PageSize="2" >
<FooterStyle BackColor="Tan"></FooterStyle>
<PagerStyle ForeColor="DarkSlateBlue" HorizontalAlign="Center" BackColor="PaleGoldenrod"></PagerStyle>
<HeaderStyle Font-Bold="True" BackColor="Tan"></HeaderStyle>
<AlternatingRowStyle BackColor="PaleGoldenrod"></AlternatingRowStyle>
<Columns>
<asp:CommandField ShowEditButton="True" ShowDeleteButton="True"></asp:CommandField>
<asp:BoundField HeaderText="Id Funcionários" DataField="IdFuncionarios" SortExpression="IdFuncionarios" ReadOnly="True"></asp:BoundField>
<asp:BoundField HeaderText="Nome" DataField="Nome" SortExpression="Nome"></asp:BoundField>
<asp:BoundField HeaderText="Observação" DataField="Observacao" SortExpression="Observacao"></asp:BoundField>
</Columns>
<SelectedRowStyle ForeColor="GhostWhite" BackColor="DarkSlateBlue"></SelectedRowStyle>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" Runat="server" TypeName="FuncionariosCollectionDB"
SelectMethod="LoadFromDb" UpdateMethod="SimpleUpdateDb" DeleteMethod="DeleteFromDb" DataObjectTypeName="Funcionarios"
ConflictDetection="OverwriteChanges" OnObjectCreating="OnObjectCreating" OnSelecting="OnSelecting">
</asp:ObjectDataSource>

Para já, vamos ignorar o porquê do processamento OnSelecting e vamos passar a explicar como é que funciona o mecanimos de edição/actualização dos dados. Como seria de esperar, temos de indicar o nome do método que irá ser responsável por efectuar as actualizações. Para tal recorremos à propriedade UpdateMethod. Temos duas hipóteses (ambas viáveis):



  • definir uma lista de parâmetros delimitadas pelo elemento UpdateParameters. Se seguirmos este caminho, temos de definir o nosso método de actualização com os mesmos parâmetros (nome e tipo).

  • alternativamente podemos definir um método de actualização que apenas recebe um parâmetro do tipo indicado através da propriedade DataObjectTypeName do controlo. Neste caso, não indicamos nenhum parâmetro no interior do elemento UpdateParameters (foi esta a opção escolhida neste sample).

Os mais atentos devem ter reparado na referência de uma nova propriedade DataObjectTypeName. O objectivo desta propriedade é definir o tipo de elementos que são retornados pelo controlo (vendo da perspectiva da grid, serve para indicar o tipo de elemento definido em cada linha). Não devemos confundir esta propriedade com a propriedade TypeName. Esta última serve apenas para definirmos qual o tipo que irá fornecer os métodos necessários às várias operações que irão ser efectuadas sobre os dados. Esta propriedade também é importante se decidirmos optar pela segunda opção (relativa à edição de componentes) pois é através dela que é instanciado um objecto que irá ser passado ao método de edição.


A actualização de dados gera um par de eventos (que podem ser ) importantes:



  • OnUpdating: evento despoletado antes da actualização efectiva dos dados. Recebe um parâmetro do tipo ObjectDataSourceMethodEventArgs que contém um IDictionary com a colecção de valores actualizados (disponibilizado através da propriedade InputParameters).

  • OnUpdated: evento gerado após a actualização efectiva (leia-se evocação do método associado à actualização) dos dados.

Foi nesta altura que encontrei aquela que é (na minha modesta opinião) a primeira grande falha deste controlo. O modelo de objectos (que estamos a utilizar) estabelece uma determinada hierarquia entre os vários elementos. Se repararem bem no código, vão verificar que cada objecto do tipo Funcionarios contém uma propriedade do tipo Telefones. Esta propriedade (do tipo TelefonesCollection) contém uma colecção de vários objectos (do tipo Telefone) que encapsulam os vários telefones associados a um funcionário. A utilização de um modelo parte do principio que as seguintes condições são verdadeiras:



  • ao carregar um funcionário, a colecção de telefones é automaticamente preenchida com todos os telefones associados a esse funcionário;

  • que a modificação dos dados de um funcionário seja (automaticamente) propagada aos contactos associados (este é um caso complexo pois durante uma operação de modificação do funcionário podemos eliminar telefones, adicionar outros ou mesmo modificar um telefone existente);

  • que a eliminação de um funcionário resulte (também) na eliminação dos telefones associados ao funcionário.

O problema que enfrentamos na actualização está relacionado com a forma como efectuamos as modificações dos dados dos funcionários. Isto deve-se à forma como o controlo ObjectDataSource funciona. Quando efectuamos uma modificação, o controlo vai construir uma nova instância da classe e vai passá-la ao nosso método associado à propriedade UpdateMethod. Contudo, o carregamento dos dados do funcionário deve ser efectuado através do método LoadFromDb (pois este é o método que preenche as propriedades e a colecção de forma correcta). O problema aqui está relacionado com o facto de não podermos personalizar o processo de criação do objecto aquando da operação de actualização,


Se verificarem o código, vão reparar que o código por mim definido propaga a modificação do funcionário para a colecção de telefones de uma forma especial. Assim, uma modificação de um funcionário corresponde a uma eliminação dos telefones associados seguida de uma inserção de todos os telefones actuais na colecção. Esta é a forma mais simples de resolver todos os problemas associados a este tipo de relacionamentos (reparem que durante uma modificação, um funcionário pode perder alguns telefones e obter novos; com esta arquitectura, conseguimos resolver todas as situações possiveis de uma forma rápida e quase indolor :) )


Incialmente tinha configurado o controlo de dados de forma a que este recorresse ao método UpdateDb aquando da actualização dos dados. Contudo, neste caso a utilização deste método é puro suicido! Isto porque quando o método for evocado, o controlo vai construir automaticamente um novo objecto do tipo funcionário (de acordo com os passos apresentados nos parágrafos anteriores) e vai preencher as suas propriedades de acordo com as alterações efectuadas. O problema prende-se com o facto da propriedade Telefones (que representa a colecção de telefones associada aos funcionários) não ser criada antes de se proceder à eliminação. Portanto, de acordo com o nosso algoritmo de modificação, todos os telefones do funcionário são apagados e depois os que se encontram na colecção são adicionados. Como a colecção não foi criada correctamente...presumo que já estão a ver onde eu quero chegar.


Claro que não posso pedir ao controlo para ter estas questões em atenção. Para isso é que existe o evento OnUpdating (pelo menos foi o que eu pensei ao ler a documentação!). Este evento recebe um parâmetro extremamente importante (do tipo ObjectDataSourceMethodEventArgs) que contém uma propriedade que nos permite aceder aos parâmetros (e respectivos valores) que irão ser passados para o método (estou a falar da propriedade InputParameters). Ao aperceber-me disto, pensei que o meu problema estava resolvido. A única coisa que tinha de fazer era alterar o valor do diccionário de forma a que este contivesse o objecto devidamente actualizado. Infelizmente, e apesar de todas as minhas tentativas (e foram algumas), não consegui configurar correctamente os valores obtidos. Parece que, ao contrário do que acontece com o evento ObjectCreating, onde de facto conseguimos criar um objecto de acordo com os nossos critérios, não é possível efectuarmos este tipo de customização neste evento (pelo menos o objecto não liga ao nosso objecto e insiste em criar uma instância desse objecto que depois passa ao método de actualização).


Não sei se este tipo de comportamento é um bug ou uma feature :) Por outro lado, pode também ser um problema semelhante ao que me aconteceu quando utilizei o DataSetDataSource (neste caso, o código funciona perfeitamente quando corre numa máquina que contém o Visual Studio 2005, mas recusa-se a funcionar na minha que possui o Visual Web Dev). Resumindo, tive de modificar a minha classe de forma a que esta contemplasse um método que apenas faz a actualização das propriedades relativas ao próprio objecto (portanto, sem propagação para a colecção de telefones).


Bom, continuando na nossa caminha (ou melhor exploração), resolvi tentar efectuar a paginação da grid. De referir que tudo correu bem, sem necessidade de escrever código adicional (portanto, tudo correu na perfeição). Cheguei então à parte da ordenação. Se o meu método associado ao Select fornecesse umDataSet, não havia necessidade de escrever uma única linha de código (o código que acompanha o artigo contém um método que permite utilizar esta estratégia). Bem, apesar de não ser muito complicado fornecer um método que retornasse um DataSet, quis ver se conseguia arranjar maneira de obter ordenação sem recorrer aos datasets.


Bem, lembram-se de eu ter dito para ignorarem o evento Selecting do controlo? A partir daqui já não o podemos ignorar e vamos ter mesmo de analisar o código (simples) associado ao evento:

private void OnSelecting( object sender, ObjectDataSourceSelectingEventArgs args )
{
args.InputParameters.Clear();
if (GridView1.SortExpression.Length != 0)
{
args.InputParameters.Add( "sortExpression", GridView1.SortExpression );
args.InputParameters.Add( "sortOrder", GridView1.SortDirection == SortDirection.Ascending ? "asc" : "desc" );
}
}

O principio de funcionamento é bastante simples. O controlo ObjectDataSource evoca um método tendo em atenção o nome do método indicado e os parâmetros definidos. Quando o evento Selecting é despoletado, podemos aceder a esses parâmetros através da propriedade InputParameters associada ao elemento do tipo ObjectDataSourceMethodEvents. O truque consiste em verificar se o utilizador pediu a ordenação da grid de acordo com uma determinada coluna. Se tal aconteceu, então a propriedade SortExpression da grid possui um valor não vazio (portanto, com cumprimento diferente de zero). Neste casos, adiciono os parâmetros necessários à evocação do overload do método LoadFromDb (o código define dois métodos LoadFromDb: um carrega todos os dados e o outro carrega os dados ordenando-os de acordo com os valores indicados). Convém notar que, para que tudo funcione de forma correcta, os valores associados às expressões de ordenação das colunas da grid têm de possui o mesmo nome dos campos existentes na base de dados (poderíamos contornar esta necessidade tornado o código do LoadFromDb um pouco mais complexo).


Falta apenas falar na eliminação. O código de eliminação não trouxe nenhum problema uma vez que o objecto Funcionarios era capaz de propagar essa eliminação para os telefones associados.


Resumindo, afirmaria que este controlo é interessante porque nos permite efecutar o binding entre uma grid e uma colecção personalizada de objectos. Contudo, acho que este controlo não é indicado para efectuar operações de adição, edição e eliminação que não sejam simples.


Conclusões finais


Ao longo deste artigo falámos sobre últimos controlos de acesso a dados que não haviam sido abordados no artigo anterior. Ao longo destes dois artigos falámos acerca das novidades que podemos encontrar nos novos controlos de acesso a dados. Apesar de termos abordado a maioria das funcionalidades, houve uma que não foi referida (e que irá ser o tópico de um artigo futuro): caching. Apesar de não ir falar de forma aprofundada acerca dessa funcionalidade aqui (fica prometido um artigo sobre esse assunto), não poderia encerrar a área relativa aos objectos de acesso a fontes de dados sem referir que estes já conseguem efectuar a cache dos dados que fornecem (desde que o programador efectue as configurações necessárias).


E assim terminamos esta fase relativa aos objectos de acesso a dados. No próximo artigo vamos falar acerca do novo controlo que substitui o DataGrid: estou a falar da GridView. Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.


Fiquem bem e boa programação! Até à próxima. O código que acompanha este artigo está disponível na secção dos downloads do site.


Leiam o meu blog em: http://members.netmadeira.com/luisabreu


Written By: labreu
Date Posted: 4/14/2006
Number of Views: 689

Return