Desenvolvimento - ASP. NET
Introdução às Dependency Properties
Quando desenvolvemos classes e/ou controles, geralmente expomos as suas características através de propriedades. Na maioria das vezes, essas propriedades encapsulam o acesso à membros privados internos, interceptando a escrita e leitura dos mesmos. Os controles ASP.NET geralmente não possuem esses membros internos, e armazenam o conteúdo diretamente no ViewState ou no ControlState da respectiva página.
por Israel AéceQuando desenvolvemos classes e/ou controles, geralmente expomos as suas características através de propriedades. Na maioria das vezes, essas propriedades encapsulam o acesso à membros privados internos, interceptando a escrita e leitura dos mesmos. Os controles ASP.NET geralmente não possuem esses membros internos, e armazenam o conteúdo diretamente no ViewState ou no ControlState da respectiva página.
As propriedades também são utilizadas por controles Windows Forms, seguindo a mesma estratégia das propriedades tradicionais que as linguagens disponibilizam. Mas aplicações Windows geralmente tem algumas características próprias, como validação e, principalmente, databinding, que é a possibilidade de vincular um objeto à algum controle de interface (UI). Há também algumas tarefas que são comuns em aplicações deste tipo, como por exemplo, a notificação da alteração de um valor, que fará eventuais mudanças nos demais controles.
Para tornar esse trabalho mais suave, a Microsoft introduziu, desde a primeira versão do WPF, as Dependency Properties. Esse tipo especial de propriedade é semelhante as propriedades das linguagens, mas com muito mais poder. A inteligência envolvida neste novo tipo de propriedade, permite trabalharmos com o código puramente declarativo (XAML), sem precisar de qualquer código procedural para efetuar alguma mudança na aparência quando algo acontecer.
Além disso, temos uma série de outros benefícios ao utilizá-las. O primeiro deles é a capacidade de sermos notificados quando o valor dessa propriedade for alterado, e o próprio sistema de Triggers do WPF faz uso desta funcionalidade, onde você consegue associar um valor à uma propriedade, e quando ela receber este valor, você poderá tomar alguma decisão, como alterar a cor de outro controle, disparo de outros eventos, etc. Há também outros pontos positivos que vamos analisar mais tarde, ainda neste artigo.
A sua utilização é relativamente simples, e não dispensa completamente o uso das propriedades tradicionais fornecidas pelas linguagens. As classes (ou controles) que querem fazer uso destas propriedades, deverão herdar direta ou indiretamente da classe DependencyObject, pois essa classe permitirá ao objeto participar do serviço de propriedades que o WPF disponibiliza. Além disso, para que uma propriedade seja registrada e, consequentemente, poder fazer uso de toda essa infraestrutura, cada propriedade exposta deverá manter um campo dentro da classe onde ela será declarada, definindo o tipo deste campo como sendo DependencyProperty. Abaixo temos a estrutura de uma dependency property:
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MeuControle));
public string Title
{
get
{
return this.GetValue(TitleProperty).ToString();
}
set
{
this.SetValue(TitleProperty, value);
}
}
O primeiro ponto que vemos é que a classe DependencyProperty não é instanciada diretamente. Para registrá-la, você precisa invocar o método estático Register (dessa mesma classe), que em sua versão mais básica, receberá o nome da propriedade, o tipo da propriedade e o tipo onde essa propriedade está sendo criada. Outro ponto bastante curioso é que esse campo é declarado como estático. Mas e se houverem várias instâncias dessa classe, o valor da propriedade será compartilhado? Não. Na verdade, as propriedades são salvas em uma espécie de dicionário, onde a chave deverá ser única dentro do tipo. Aqui é a grande diferença, ou seja, ao invés de termos um campo privado para cada propriedade exposta, que muitas vezes ficavam com o seu valor padrão, as dependency properties resolvem este problema armazenando somente os valores que são modificados pela instância, enquanto as outras compartilham um - mesmo - valor padrão, reutilizando o valor padrão por todas as instâncias.
A seguir temos a propriedade Title, que por sua vez, serve apenas como um wrapper para o objeto que criamos acima, expondo o tipo efetivo da propriedade, que no caso acima é string. Note que em momento nenhum você acessa o objeto TitleProperty diretamente; toda a manipulação é realizada pelos métodos GetValue e SetValue, para ler e escrever, respectivamente. Como vimos, esses métodos estão disponíveis para todas as classes que derivam da classe DependencyObject, que realiza todas as etapas necessárias para determinar se o valor já foi sobrescrito pela instância corrente, e caso tenha sido, este será retornado ao invés de seu valor padrão.
Ainda há a possibilidade de registrar uma dependency property como somente leitura, pois pode haver propriedades que apenas reportam o estado interno da classe, como por exemplo, a propriedade IsMouseOver da classe Control, que retorna um valor boleano indicando se o ponteiro está ou não posicionado em cima daquele controle. Neste caso, o valor é definido internamente pelo WPF.
A classe DependencyProperty fornece para essa finalidade, um método também estático, chamado RegisterReadOnly, que depois de registrado, retornará uma instância da classe DependencyPropertyKey, representando a chave para a propriedade recém registrada.
private static readonly DependencyPropertyKey TitlePropertyKey =
DependencyProperty.RegisterReadOnly(
"Title",
typeof(string),
typeof(MeuControle));
public static readonly DependencyProperty TitleProperty =
TitlePropertyKey.DependencyProperty;
public string Title
{
get
{
return this.GetValue(TitleProperty).ToString();
}
private set
{
this.SetValue(TitlePropertyKey, value);
}
}
Neste caso, veja que a chave é declarada de forma privada, para que somente o interior da classe onde ela é declarada tenha acesso. A criação do objeto DependencyProperty ainda é necessário, mas não será através do método Register. A classe DependencyPropertyKey define uma propriedade chamada DependencyProperty, que retorna a propriedade para acessá-la através do método GetValue, seguindo o mesmo esquema que vimos acima. Na propriedade Title, a escrita também está protegida pelo modificador private, e note que a alteração está sendo feita, passando o chave e não a instância da DependencyProperty como fizemos no primeiro exemplo.
Metadados
Como vimos acima, a classe DependencyProperty ainda fornece outras versões (overloads) do método Register. O que veremos a seguir, é aquele que recebe os metadados. Através destes metadados, poderemos configurar uma série de características destas propriedades, como por exemplo: valor padrão, notificação quando o valor for alterado, coerção, etc. A principal classe que podemos utilizar para definição destes metadados, é a FrameworkPropertyMetadata.
Entre as principais funcionalidades que esta classe fornece, temos a possibilidade de informar o valor padrão da propriedade, e enquanto você não definir um novo valor para uma propriedade, será este valor que será retornado. Além disso, podemos definir através de delegates, ações que desejamos executar quando o valor for alterado ou alguma ação para efetuar uma coerção no valor. Abaixo temos o exemplo de como utilizar esses metadados:
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MeuControle),
new FrameworkPropertyMetadata(
"Título Indefinido",
(o, e) => MessageBox.Show(e.NewValue.ToString()),
(d, e) => string.Format("*** {0} ***", e)));
O primeiro parâmetro informado no construtor da classe FrameworkPropertyMetadata, é o valor padrão da propriedade. Esse parâmetro é do tipo System.Object, pois a propriedade poderá ser de qualquer tipo. Todas as instâncias desta classe sempre compartilharão este mesmo valor, a menos que uma instância específica o sobrescreva, e a partir daí, esta instância terá o seu próprio valor.
Os dois parâmetros seguintes são delegates, onde o primeiro deles (PropertyChangedCallback) representa o método que será disparado quando o valor desta propriedade for alterado. O argumento "e" é do tipo DependencyPropertyChangedEventArgs, e entre suas propriedades, temos OldValue e NewValue, que são autoexplicativas. Já o terceiro parâmetro (CoerceValueCallback), define um método de coerção, que tem a finalidade de customizar/formatar a entrada da informação de acordo com alguma regra, que acontecerá antes de armazenar o valor definitivamente. O "e" que vemos sendo passado para este método, traz o valor atual; a assinatura deste delegate também retorna um System.Object, que é justamente o valor depois de alterado.
Validação
Outro ponto importante quando falamos sobre propriedades, é a questão de validação. As validações geralmente são avaliadas antes do valor ser efetivamente armazenado. As dependency properties também já trazem a validação nativamente, que podemos configurar e vincular durante a criação de uma propriedade.
Para isso, o método Register ainda fornece em um de seus overloads, um parâmetro do tipo ValidateValueCallback, que também é um delegate e que recebe o valor que está sendo passado para a propriedade e retorna um valor boleano, indicando se o parâmetro está válido ou não. O seu uso está sendo exibido abaixo, e note que antes da propriedade receber o valor, a consistência para verificar se ele está correta será avaliada, e caso retorne False, uma exceção será disparada.
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MeuControle),
new FrameworkPropertyMetadata(
"Sem Título Definido",
(o, e) => MessageBox.Show(e.NewValue.ToString()),
(d, e) => string.Format("*** {0} ***", e)),
p => p !=null && p.ToString().Length > 0);
Conclusão: Apesar de no primeiro momento tornar o código um pouco ilegível, percebermos no decorrer deste artigo o poder deste tipo de propriedade. Grande parte das tarefas de manipulação de UI, e que antes ficavam espalhadas pelo código, agora podemos centralizar, tornando a manutenção e reutilização muito mais simples. Isso sem contar que utilizar esse tipo de propriedade habilita grande parte dos recursos exclusivos do WPF, como estilos, databinding, etc.
As propriedades também são utilizadas por controles Windows Forms, seguindo a mesma estratégia das propriedades tradicionais que as linguagens disponibilizam. Mas aplicações Windows geralmente tem algumas características próprias, como validação e, principalmente, databinding, que é a possibilidade de vincular um objeto à algum controle de interface (UI). Há também algumas tarefas que são comuns em aplicações deste tipo, como por exemplo, a notificação da alteração de um valor, que fará eventuais mudanças nos demais controles.
Para tornar esse trabalho mais suave, a Microsoft introduziu, desde a primeira versão do WPF, as Dependency Properties. Esse tipo especial de propriedade é semelhante as propriedades das linguagens, mas com muito mais poder. A inteligência envolvida neste novo tipo de propriedade, permite trabalharmos com o código puramente declarativo (XAML), sem precisar de qualquer código procedural para efetuar alguma mudança na aparência quando algo acontecer.
Além disso, temos uma série de outros benefícios ao utilizá-las. O primeiro deles é a capacidade de sermos notificados quando o valor dessa propriedade for alterado, e o próprio sistema de Triggers do WPF faz uso desta funcionalidade, onde você consegue associar um valor à uma propriedade, e quando ela receber este valor, você poderá tomar alguma decisão, como alterar a cor de outro controle, disparo de outros eventos, etc. Há também outros pontos positivos que vamos analisar mais tarde, ainda neste artigo.
A sua utilização é relativamente simples, e não dispensa completamente o uso das propriedades tradicionais fornecidas pelas linguagens. As classes (ou controles) que querem fazer uso destas propriedades, deverão herdar direta ou indiretamente da classe DependencyObject, pois essa classe permitirá ao objeto participar do serviço de propriedades que o WPF disponibiliza. Além disso, para que uma propriedade seja registrada e, consequentemente, poder fazer uso de toda essa infraestrutura, cada propriedade exposta deverá manter um campo dentro da classe onde ela será declarada, definindo o tipo deste campo como sendo DependencyProperty. Abaixo temos a estrutura de uma dependency property:
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MeuControle));
public string Title
{
get
{
return this.GetValue(TitleProperty).ToString();
}
set
{
this.SetValue(TitleProperty, value);
}
}
O primeiro ponto que vemos é que a classe DependencyProperty não é instanciada diretamente. Para registrá-la, você precisa invocar o método estático Register (dessa mesma classe), que em sua versão mais básica, receberá o nome da propriedade, o tipo da propriedade e o tipo onde essa propriedade está sendo criada. Outro ponto bastante curioso é que esse campo é declarado como estático. Mas e se houverem várias instâncias dessa classe, o valor da propriedade será compartilhado? Não. Na verdade, as propriedades são salvas em uma espécie de dicionário, onde a chave deverá ser única dentro do tipo. Aqui é a grande diferença, ou seja, ao invés de termos um campo privado para cada propriedade exposta, que muitas vezes ficavam com o seu valor padrão, as dependency properties resolvem este problema armazenando somente os valores que são modificados pela instância, enquanto as outras compartilham um - mesmo - valor padrão, reutilizando o valor padrão por todas as instâncias.
A seguir temos a propriedade Title, que por sua vez, serve apenas como um wrapper para o objeto que criamos acima, expondo o tipo efetivo da propriedade, que no caso acima é string. Note que em momento nenhum você acessa o objeto TitleProperty diretamente; toda a manipulação é realizada pelos métodos GetValue e SetValue, para ler e escrever, respectivamente. Como vimos, esses métodos estão disponíveis para todas as classes que derivam da classe DependencyObject, que realiza todas as etapas necessárias para determinar se o valor já foi sobrescrito pela instância corrente, e caso tenha sido, este será retornado ao invés de seu valor padrão.
Ainda há a possibilidade de registrar uma dependency property como somente leitura, pois pode haver propriedades que apenas reportam o estado interno da classe, como por exemplo, a propriedade IsMouseOver da classe Control, que retorna um valor boleano indicando se o ponteiro está ou não posicionado em cima daquele controle. Neste caso, o valor é definido internamente pelo WPF.
A classe DependencyProperty fornece para essa finalidade, um método também estático, chamado RegisterReadOnly, que depois de registrado, retornará uma instância da classe DependencyPropertyKey, representando a chave para a propriedade recém registrada.
private static readonly DependencyPropertyKey TitlePropertyKey =
DependencyProperty.RegisterReadOnly(
"Title",
typeof(string),
typeof(MeuControle));
public static readonly DependencyProperty TitleProperty =
TitlePropertyKey.DependencyProperty;
public string Title
{
get
{
return this.GetValue(TitleProperty).ToString();
}
private set
{
this.SetValue(TitlePropertyKey, value);
}
}
Neste caso, veja que a chave é declarada de forma privada, para que somente o interior da classe onde ela é declarada tenha acesso. A criação do objeto DependencyProperty ainda é necessário, mas não será através do método Register. A classe DependencyPropertyKey define uma propriedade chamada DependencyProperty, que retorna a propriedade para acessá-la através do método GetValue, seguindo o mesmo esquema que vimos acima. Na propriedade Title, a escrita também está protegida pelo modificador private, e note que a alteração está sendo feita, passando o chave e não a instância da DependencyProperty como fizemos no primeiro exemplo.
Metadados
Como vimos acima, a classe DependencyProperty ainda fornece outras versões (overloads) do método Register. O que veremos a seguir, é aquele que recebe os metadados. Através destes metadados, poderemos configurar uma série de características destas propriedades, como por exemplo: valor padrão, notificação quando o valor for alterado, coerção, etc. A principal classe que podemos utilizar para definição destes metadados, é a FrameworkPropertyMetadata.
Entre as principais funcionalidades que esta classe fornece, temos a possibilidade de informar o valor padrão da propriedade, e enquanto você não definir um novo valor para uma propriedade, será este valor que será retornado. Além disso, podemos definir através de delegates, ações que desejamos executar quando o valor for alterado ou alguma ação para efetuar uma coerção no valor. Abaixo temos o exemplo de como utilizar esses metadados:
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MeuControle),
new FrameworkPropertyMetadata(
"Título Indefinido",
(o, e) => MessageBox.Show(e.NewValue.ToString()),
(d, e) => string.Format("*** {0} ***", e)));
O primeiro parâmetro informado no construtor da classe FrameworkPropertyMetadata, é o valor padrão da propriedade. Esse parâmetro é do tipo System.Object, pois a propriedade poderá ser de qualquer tipo. Todas as instâncias desta classe sempre compartilharão este mesmo valor, a menos que uma instância específica o sobrescreva, e a partir daí, esta instância terá o seu próprio valor.
Os dois parâmetros seguintes são delegates, onde o primeiro deles (PropertyChangedCallback) representa o método que será disparado quando o valor desta propriedade for alterado. O argumento "e" é do tipo DependencyPropertyChangedEventArgs, e entre suas propriedades, temos OldValue e NewValue, que são autoexplicativas. Já o terceiro parâmetro (CoerceValueCallback), define um método de coerção, que tem a finalidade de customizar/formatar a entrada da informação de acordo com alguma regra, que acontecerá antes de armazenar o valor definitivamente. O "e" que vemos sendo passado para este método, traz o valor atual; a assinatura deste delegate também retorna um System.Object, que é justamente o valor depois de alterado.
Validação
Outro ponto importante quando falamos sobre propriedades, é a questão de validação. As validações geralmente são avaliadas antes do valor ser efetivamente armazenado. As dependency properties também já trazem a validação nativamente, que podemos configurar e vincular durante a criação de uma propriedade.
Para isso, o método Register ainda fornece em um de seus overloads, um parâmetro do tipo ValidateValueCallback, que também é um delegate e que recebe o valor que está sendo passado para a propriedade e retorna um valor boleano, indicando se o parâmetro está válido ou não. O seu uso está sendo exibido abaixo, e note que antes da propriedade receber o valor, a consistência para verificar se ele está correta será avaliada, e caso retorne False, uma exceção será disparada.
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title",
typeof(string),
typeof(MeuControle),
new FrameworkPropertyMetadata(
"Sem Título Definido",
(o, e) => MessageBox.Show(e.NewValue.ToString()),
(d, e) => string.Format("*** {0} ***", e)),
p => p !=null && p.ToString().Length > 0);
Conclusão: Apesar de no primeiro momento tornar o código um pouco ilegível, percebermos no decorrer deste artigo o poder deste tipo de propriedade. Grande parte das tarefas de manipulação de UI, e que antes ficavam espalhadas pelo código, agora podemos centralizar, tornando a manutenção e reutilização muito mais simples. Isso sem contar que utilizar esse tipo de propriedade habilita grande parte dos recursos exclusivos do WPF, como estilos, databinding, etc.