Desenvolvimento - WCF/WPF

DataBinding em WPF

A Microsoft incorporou no WPF uma forma muito mais evoluída para efetuar databinding, não se limitando apenas a vincular uma fonte de dados à controles, mas também permitindo que qualquer objeto preencha outro, incluindo controles.

por Israel Aéce



Já ouvimos falar muito sobre o termo DataBinding. Como sabemos, trata-se de um mecanismo para associar a informação de uma determinada origem à um determinado destino, criando uma dependência unidirecional ou bidirecional. Muitas vezes este termo está associado à alguma fonte de dados, que desejamos exibir suas respectivas informações na tela de uma aplicação qualquer.

Cada tecnologia implementa isso de uma forma diferente, com seus benefícios e possíveis limitações. O ASP.NET traz essa funcionalidade, onde você pode facilmente ligar uma fonte de dados, mas tem algumas limitações por conta de ser HTTP, que não mantém estado. Já o Windows Forms, fornece uma forma de databinding muito mais rico em termos de funcionalidades. A finalidade deste artigo é apresentar o databinding no WPF, exibindo suas funcionalidades, que facilitarão a forma com que lidamos com manipulações de UI.

A Microsoft incorporou no WPF uma forma muito mais evoluída para efetuar databinding, não se limitando apenas a vincular uma fonte de dados à controles, mas também permitindo que qualquer objeto preencha outro, incluindo controles. Isso quer dizer que poderemos definir o valor de uma propriedade de um controle com o valor de uma outra propriedade e de um outro controle. Tudo isso eliminando grande parte do códigoimperativo requerido pelo Windows Forms, pois a partir de agora, poderemos recorrer a código declarativo (XAML) para especificar essas "amarrações".

Grande parte da responsabilidade para fazer tudo isso funcionar, é a classe Binding, que está debaixo do namespace System.Windows.Data. Ela é responsável por manter o "canal de comunicação" entre a origem e o destino, e além disso, expõe uma série de propriedades que nos permite customizar o comportamento dessa comunicação. Entre as principais propriedades, temos:

  • ElementName: define o nome do elemento que servirá como fonte. Utilize esta propriedade quando desejar preencher uma outra propriedade com o valor de um controle do WPF.
  • Mode: determina a direção das informações.
  • NotifyOnSourceUpdated: valor boleano indicando se o evento SourceUpdated é disparado quando alguma atualização na fonte das informações ocorrer.
  • NotifyOnTargetUpdated: valor boleano indicando se o evento SourceUpdated é disparado quando alguma atualização no destino das informações ocorrer.
  • Path: espefica o nome da propriedade que será exibida.
  • RelativeSource: especifica uma fonte de forma relativa à posição do objeto atual.
  • Source: define o nome do objeto que servirá como fonte. Utilize esta propriedade quando desejar preencher com uma instância de um objeto.
  • XPath: a mesma finalidade da propriedade Path, mas define uma expressão XPath quando a fonte de informações for um arquivo Xml.

Note que nas propriedades acima, nós não temos uma propriedade que especifica qual propriedade no destino será carregada. Isso se deve, porque você aplicará a sintaxe de binding diretamente dentro da propriedade que você quer preencher. O exemplo abaixo mostra como podemos proceder para preencher um conteúdo de um controle Label com o texto de um Button:

<Button Name="button1" Content="Texto do Botão" />
<Label Name="label1" Content="{Binding ElementName=button1, Path=Content}" />

Alternativamente, você pode achar essa sintaxe um pouco ilegível, principalmente quando você tiver situações mais complexas. Se desejar, você pode recorrer à uma segunda forma de configurar o databinding, de forma hierárquica, onde você irá aninhar as configurações como um Xml tradicional, através de sub-elementos. O código abaixo ilustra esta segunda técnica:

<Button Name="button1" Content="Texto do Botão" />
<Label Name="label1">
<Label.Content>
<Binding ElementName="button1" Path="Content" />
</Label.Content>
</Label>

Quando o modelo declarativa não é uma solução, pois você precisa dinamicamente determinar os databindings, você pode ainda utilizar o código C#/VB.NET para configurá-los. Tudo o que precisamos fazer é instanciar a classe Binding que falamos acima, e configurar as propriedades necessárias para que isso funcione, e que neste exemplo simples serão ElementName e Path. ElementName vai receber uma string com o nome do controle que servirá como origem, enquanto a propriedade Path, receberá a instância da classe PropertyPath, que em seu construtor você deverá especificar o nome da propriedade no objeto de origem, que quer que seja enviado para o destino.

Binding b = new Binding();
b.ElementName = "button1";
b.Path = new PropertyPath("Content");

this.label1.SetBinding(Label.ContentProperty, b);

Depois da instância da classe Binding configurada, utilizamos o método SetBinding do controle de destino, que no caso do exemplo é o Label. Além do Binding, esse método recebe a dependency property que receberá o valor da origem.

Carregando um Objeto

Acima vimos como podemos utilizar o databinding de uma forma diferente da qual estamos acostumado, que é através de controles de UI. Mas, o cenário mais comum é quando precisamos preencher um, ou vários controles de UI, com propriedades de um objeto. Eventualmente você tem um objeto que foi construído pela aplicação, e você precisa exibí-lo no formuário, distribuindo suas propriedades pelos controles do mesmo. O databinding também ajuda nisso, onde você pode especificar o tipo da classe, e o próprio WPF o instancia e, consequentemente, preenche os controles interessados, e tudo isso sendo feito declarativamente.

Ao invés de utilizar a propriedade ElementName, vamos agora recorrer à propriedade Source, que deve ser utilizada quando a origem se tratar de um objeto. Para exemplificar, vamos criar a instância da classe dentro dos resources do formulário, que nada mais é que uma coleção que pode armazenar qualquer tipo de objeto. Teremos uma classe simples chamada de Configuracao, contendo uma propriedade chamada Url. A instância desta classe será criada pelo WPF e estará armazenada estaticamente dentro dos recursos locais daquele formulário.

A sintaxe de binding agora consiste em configurar a propriedade Source com a instância criada e nomeada como "config". Continuamos a utilizar a propriedade Path, mas agora ela deverá refletir a propriedade do objeto que será preenchida pelo controle. Como podemos perceber, a propriedade Text do TextBox irá exibir a propriedade Url:

<Window x:Class="Teste.Window2"
xmlns:local="clr-namespace:Teste">
<Window.Resources>
<local:Configuracao x:Key="config" />
</Window.Resources>
<Grid name="grid1">
<TextBox Name="textBox1" Text="{Binding Source={StaticResource config}, Path=Url}" />
</Grid>
</Window>

Suponhamos que temos vários controles e cada um receberá o valor de uma propriedade diferente. Ao invés de ficar repetindo a instância do objeto (config), podemos utilizar a propriedade DataContext. Essa propriedade nos permite compartilhar a mesma fonte por vários controles, e cada controle que o utilizará, apenas deve indicar qual propriedade ele estará vinculado. Repare que vinculamos o config à propriedade DataContext do controle Grid, e os controles inerentes à eles apenas mencionam qual propriedade cada um deles quer utilizar, sem a necessidade de especificar a propriedade Source.

<Window x:Class="Teste.Window2"
xmlns:local="clr-namespace:Teste">
<Window.Resources>
<local:Configuracao x:Key="config" />
</Window.Resources>
<Grid name="grid1" DataContext="{StaticResource config}">
<TextBox Name="textBox1" Text="{Binding Path=Url}" />
<TextBox Name="textBox2" Text="{Binding Path=Timeout}"/>
</Grid>
</Window>

Um detalhe importante aqui, é que se um controle não especificar nenhuma das propriedades de Binding (Source, RelativeSource ou Element), o WPF procura por algum elemento que possui a propriedade DataContext definida na árvore visual do formulário, e encontrando-o, tentará extrair o valor dele. Essa propriedade também pode ser configurada de via código, caso a instância do objeto precise de alguma manipulação adicional antes de ser usada pelo WPF.

this.grid1.DataContext = new Configuracao();

ObjectDataProvider

Como vimos acima, podemos criar a instância do objeto via código. Geralmente recorremos a essa técnica quando precisamos customizar a criação deste objeto. Mas o WPF fornece uma opção, que nos permite customizar de forma declarativa, algumas opções que podem ser utilizadas durante a criação deste objeto.

Entre essas opções, podemos definir alguns parâmetros para um construtor, fazer o databinding através de um método que retorna a instância do objeto, entre outras opções. Suponhamos que a partir de agora o construtor da classe Configuracao recebe como parâmetro uma string com a Url. Se tentar rodar a aplicação sem qualquer alteração, uma exceção será lançada dizendo que a classe não possui nenhum construtor público sem parâmetros. O ObjectDataProvider irá nos ajudar, permitindo especificar o valor deste parâmetro durante a criação:

<Window x:Class="Teste.Window2"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:Teste">
<Window.Resources>
<ObjectDataProvider x:Key="config" ObjectType="{x:Type local:Configuracao}">
<ObjectDataProvider.ConstructorParameters>
<system:String>http://wwww.israelaece.com</system:String>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid DataContext="{StaticResource config}">
<TextBox Name="textBox1" Text="{Binding Path=Url}" />
<TextBox Name="textBox2" Text="{Binding Path=Timeout}"/>
</Grid>
</Window>

RelativeSource

Já falamos das formas de databinding ElementName e Source, mas ainda nos resta uma terceira forma: a RelativeSource. Essa forma permite especificar como fonte, um objeto que está relativamente posicionado em relação ao objeto corrente. Essa opção traz três propriedades: Mode, AncestorType e AncestorLevel. A primeira delas recebe uma entre as seguintes opções:

  • FindAncestor: refere-se à um ancestral na árvore visual, que está acima em relação ao controle corrente. Essa opção exige que as propriedades AncestorType e AncestorLevel sejam definidas. A propriedade AncestorType define o tipo de elemento que será procurado, enquanto a propriedade AncestorLevel determinao nível de profundidade da busca.
  • PreviousData: permite referenciar o item anterior de uma coleção que está sendo exibida. Útil em templates.
  • Self: permite referenciar qualquer propriedade do objeto corrente.
  • TemplatedParent: permite referenciar um objeto que está envolvido em uma template.

O exemplo abaixo ilustra o uso do FindAncestor. Note que especificamos através do AncestorType o tipo de controle que desejamos procurar e o AncestorLevel, um número inteiro que determina até quantos níveis acima a busca será realizada. Neste caso, estamos interessados em exibir como o texto do botão o valor da propriedade Name de um Grid, e como estamos definindo o nível 2, ele irá apresentar o valor "g1" no botão.

<Window>
<Grid Name="g1">
<Grid Name="g2">
<Button Name="button1">
<Button.Content>
<Binding Path="Name">
<Binding.RelativeSource>
<RelativeSource Mode="FindAncestor" AncestorType="Grid" AncestorLevel="2" />
</Binding.RelativeSource>
</Binding>
</Button.Content>
</Button>
</Grid>
</Grid>
</Window>

Binding.Mode e Binding.UpdateSourceTrigger

Vimos até agora como podemos efetuar a ligação entre a origem e o destino das informações, mas não se resume a isso.Uma das característica dodatabinding permite também customizar que possíveis alterações sejam efetuadas para refletí-las tanto na origem quanto no destino. A propriedade Mode da classe Binding nos permite configurar como responder à essas ações, onde devemos escolher uma entre as cinco opções expostas pelo enumerador BindingMode:

  • Default: especifica que o binding utilizará o modo padrão estipulado pelo destino.
  • OneTime: especifica que o binding deve atualizar o destino quando a aplicação inicia ou quando os dados mudam, mas não deve atualizar o alvo quando subsequentes alterações são feitas na origem.
  • OneWay: especifica que o binding atualizará o destino quando a origem mudar. Alterações no destino não terão efeito na origem.
  • OneWayToSource: especifica que o binding atualizará a origem quando o destino mudar. Alterações na origem não terão efeito no destino.
  • TwoWay: especifica que as alterações feitas tanto na origem quanto no destino serão atualizadas automaticamente.

Outra propriedade que também é exposta pela classe Binding é a UpdateSourceTrigger, e que é utilizada quando a propriedade Mode é definida como OneWayToSource ou TwoWay. Essa propriedade determina como e quando a atualização das informações será realizada. Essa propriedade também receberá a informação oriunda de um enumerador, chamado UpdateSourceTrigger, onde as possíveis opções são:

  • Default: indica que a atualização será de acordo com o valor definido pela propriedade de destino, que muitas vezes é PropertyChanged. Propriedades que são editáveis pelo usuário, como a propriedade Text do TextBox, define o padrão como sendo LostFocus.
  • PropertyChanged: a fonte é atualizada quando a propriedade do destino é alterada.
  • LostFocus: a fonte é atualizada quando a propriedade de destino é alterada e quando o objeto perde o foco.
  • Explicit: a atualização da fonte será realizada quando você invocar explicitamente o método UpdateSource da classe Binding.

Coleções

Muitas vezes temos coleções de objetos que desejamos exibir através de controles, como por exemplo, ListBox. Da mesma forma que vimos anteriormente, para coleções, podemos proceder de forma semelhante, mas por se tratar de coleções, temos algumas novas propriedades que são exclusivas para esse cenário.

Controles que são considerados databound, expõe as seguintes propriedades DisplayMemberPath, ItemsSource e ItemTemplate. A primeira delas define o nome da propriedade do objeto que será exibida. Já a segunda representa a coleção que contém os itens que serão definidos com fonte das informações. Finalmente, a propriedade ItemTemplate, como o próprio nome diz, nos permite criar uma forma diferenciada para exibição de cada item da coleção. Dado uma coleção de clientes, onde cada elemento é representado por uma instância da classe Cliente, podemos fazer o seguinte:

<Window x:Class="Teste.Window3"
xmlns:local="clr-namespace:Teste">
<Window.Resources>
<local:ColecaoDeClientes x:Key="cc" />
</Window.Resources>
<Grid>
<ListBox
Name="listBox1"
ItemsSource="{Binding Source={StaticResource cc}}"
DisplayMemberPath="Nome" />
</Grid>
</Window>

O binding de coleções não está restrito à controles databound. Você pode também vincular uma coleção à um controle do tipo Label, mas como já era de se esperar, apenas o primeiro elemento será exibido. Para que se consiga navegar pelos elementos da coleção, você precisa recorrer à um mecanismo exposto pelo WPF que permite essa navegação.

Quando uma coleção éutilizada através dodatabinding, o WPF cria nos bastidores um objeto que implementa a interface ICollectionView (namespace System.ComponentModel). Essa interface disponibiliza membros que permite gerenciar a navegação, ordenação e agrupamento das informações. Para extrair este objeto, podemos utilizar o seguinte código:

ColecaoDeClientes cc =new ColecaoDeClientes();
ICollectionView view = CollectionViewSource.GetDefaultView(cc);

Entre os vários membros expostos por essa interface, temos: MoveCurrentToNext, MoveCurrentToPrevious, CurrentItem, etc. Não há o que comentar sobre cada um deles, pois são autoexplicativos. De posse da instância deste navegador, podemos navegar pelos registros de forma simples, sem precisar manualmente armazenar e incrementar ou decrementar índices na medida que o usuário for solicitando a visualização de um novo registro.

Data Templates

Muitas vezes, a visualização padrão fornecida por um controle databound não nos atende, ou por questões visuais ou porque a informação precisa ser customizada/formatada para cada item. Felizmente o WPF separa a funcionalidade do controle da sua visualização, permitindo que se customize completamente a aparência, sem perder ou ter que reescrever a funcionalidade de iteração de elementos.

Como o próprio nome diz, as data templates permite customizar a aparência de um controle, configurando como queremos que ele seja exibido. Tradicionalmente o ListBox exibe cada item um abaixo do outro, sem qualquer customização. Mas e se quisermos que cada elemento seja exibido como um TextBox, e dentro da propriedade Text termos a propriedade Nome vinculada? Abaixo podemos atingir esse objetivo com as data templates, onde customizamos o ListBox e para cada item, exibimos o valor da propriedade Nome dentro da propriedade Text de um TextBox. Repare que neste caso não utilizamos a propriedade DisplayMemberPath do ListBox, pois isso foi delegado ao template, para que ele determine onde e como mostrará o valor. Para quem já trabalhou com ASP.NET, mais precisamente com os controles DataList e Repeater, notará uma grande semelhança aqui.

<Window x:Class="Teste.Window3"
xmlns:local="clr-namespace:Teste">
<Window.Resources>
<local:ColecaoDeClientes x:Key="cc" />
</Window.Resources>
<Grid>
<ListBox Name="listBox1" ItemsSource="{Binding Source={StaticResource cc}}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=Nome}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

ADO.NET

Para preencher os controles do formulário com dados de um banco de dados, você pode recorrer as mesmas técnicas apresentadas aqui. DataSets possuem uma forma tranquila para uso em databinding, mesmo no WPF. Você pode utilizar a propriedade DataContext que vimos acima, definindo para ela a instância do DataSet/DataTable com os dados carregados de um banco de dados qualquer, e a partir daí, utiliza-se a mesma sintaxe para exibição dessas informações no formulário.

Já quando trabalhamos com tecnologiasmais recentes, como oLINQ To SQL ou Entity Framework, eles sempre geram coleções, e estas estariam vinculadas aos controles que fossem exibí-las.

Conclusão: Este artigo demonstrou superficialmente todo o poder do databinding do WPF. Vimos como podemos utilizar as mais variadas formas de preencher um controle, não somente com dados de um banco de dados, mas também com informações que são geradas por outros controles ou até mesmo por outros objetos. Esse novo modelo de trabalho facilita bastante a forma como efetuamos a ligação das informações, algo que é um pouco mais complicado em seu principal concorrente, o Windows Forms.

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.