Definindo Bindable Properties
Suponha que você gostaria de uma classe Label avançada que permite que você especifique o tamanho das fontes em unidades de pontos. Vamos chamar essa classe AltLabel de "Label alternativa." Ela deriva de Label e inclui uma nova propriedade denominada PointSize.
PointSize deve ser apoiada por uma propriedade bindable? Claro! (Embora as vantagens reais de fazê-la não irá ser demonstrada até os próximos capítulos.)
O único código da classe AltLabel está incluído na biblioteca Xamarin.FormsBook.Toolkit por isso é acessível a múltiplas aplicações. A nova propriedade PointSize é implementada com um objeto erty BindableProp - chamado PointSizeProperty e uma propriedade CLR chamada PointSize que referencia PointSizeProperty:
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty … ;
…
public double PointSize
{
set { SetValue(PointSizeProperty, value); }
get { return (double)GetValue(PointSizeProperty); }
}
…
}
Ambas as definições de propriedade devem ser públicas.
Porque PointSizeProperty é definido como estática e somente leitura, ele deve ser atribuído quer em um construtor estático ou para a direita definição de campo, após o que não pode ser alterado. Geralmente, um objeto BindableProperty é atribuído na definição de campo utilizando o método BindableProperty.Create estática. Quatro argumentos são necessários (mostrado aqui com os nomes de argumentos):
propertyName — O nome de texto da propriedade (neste caso "PointSize")
returnType — O tipo da propriedade (um duplo neste exemplo)
declaringType — O tipo da classe que define a propriedade (AltLabel)
defaultValue — Um valor padrão (digamos 8 pontos)
Geralmente, o segundo e terceiro argumentos são definidos com expressões typeof. Aqui está a atribuição indicação com estes quatro argumentos passados para BindableProperty.Create:
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty = BindableProperty.Create("PointSize", // propertyName
typeof(double), // returnType typeof(AltLabel), // declaringType
8.0, // defaultValue
…);
…
}
Observe que o valor padrão é especificado como 8,0, em vez de apenas 8. Porque BindableProperty.Create é projetado para lidar com propriedades de qualquer tipo, o parâmetro é definido como defaultValue objeto. Quando o compilador C# encontra apenas um conjunto de 8 como sendo este argumento, ele assumirá que o 8 é um int e passa um int para o método. O problema não será revelado até a execução, no entanto, quando o método BindableProperty.Create estará esperando o valor padrão para ser do tipo duplo e responder levantando um TypeInitializationException.
Você deve ser explícito sobre o tipo do valor que você está especificando como padrão. Não fazê-lo é um erro muito comum na definição de propriedades vinculáveis.
BindableProperty.Create também tem seis argumentos opcionais. Aqui estão eles com os nomes de argumentos e sua finalidade:
defaultBindingMode — usado em ligação com a ligação de dados
validateValue — um retorno de chamada para verificar se há um valor válido
propertyChanged — uma chamada de retorno para indicar quando a propriedade foi alterada
propertyChanging — uma chamada de retorno para indicar quando a propriedade está prestes a mudar
coerceValue — um retorno de chamada para coagir um valor definido para outro valor (por exemplo, para restringir os valores para um intervalo)
defaultValueCreator — um retorno de chamada para criar um valor padrão. Esta função é geralmente utilizada para instanciar um objeto padrão que não pode ser compartilhado entre todas as instâncias da classe, por exemplo, um objeto de coleta, tais como a lista ou dicionário.
Não execute qualquer validação, coerção ou de propriedade alterado manipulação na propriedade CLR. A propriedade CLR deve ser restrita a chamadas SetValue e GetValue. Tudo o resto deve ser feito nos retornos proporcionados pela infraestrutura de propriedade bindable.
É muito raro que uma chamada especial para BindableProperty.Create precisaria de todos estes argumentos opcionais. Por essa razão, esses argumentos opcionais são comumente indicados com o recurso argumento nomeado introduzido em C#4.0. Para especificar um argumento opcional em particular, use o nome do argumento seguido por dois pontos. Por exemplo:
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty = BindableProperty.Create("PointSize", // propertyName
typeof(double), // returnType typeof(AltLabel), // declaringType
8.0, // defaultValue
propertyChanged: OnPointSizeChanged);
…
}
Sem dúvida, propertyChanged é o mais importante dos argumentos opcionais porque a classe usa esse retorno de chamada para ser notificado quando as alterações de propriedade, seja diretamente de uma chamada para SetValue ou através da propriedade CLR.
Neste exemplo, o manipulador de propriedade alterada é chamado OnPointSizeChanged. Ele será chamado somente quando a propriedade realmente muda, e não quando é simplesmente definido para o mesmo valor. No entanto, porque OnPointSizeChanged é referenciado a partir de um campo estático, o próprio método deve também ser estático. Aqui está o que parece:
public class AltLabel : Label
{
…
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
…
}
…
}
Isto parece um pouco estranho. Poderíamos ter várias instâncias AltLabel em um programa, ainda, sempre que a propriedade Pointsize altera em qualquer um desses casos, esse mesmo método estático é chamado. Como o método de saber exactamente qual instância AltLabel mudou?
O método sabe porque é sempre o primeiro argumento. Esse primeiro argumento é, na verdade, do tipo AltLabel, e indica que a propriedade de AltLabel instância mudou. Isso significa que você pode lançar com segurança o primeiro argumento para uma instância AltLabel:
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
AltLabel altLabel = (AltLabel)bindable;
…
}
Você pode fazer referência alguma coisa no caso particular de AltLabel cuja propriedade foi alterada. Os segundo e terceiro argumentos são realmente do tipo double para este exemplo, e indicar o prévio valor e o novo valor.
Muitas vezes é conveniente para este método estático para chamar um método de instância com os argumentos convertidos com seus tipos reais:
public class AltLabel : Label
{
…
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
((AltLabel)bindable).OnPointSizeChanged((double)oldValue, (double)newValue);
}
void OnPointSizeChanged(double oldValue, double newValue)
{
…
}
}
O método de instância pode então fazer uso de quaisquer propriedades de instância ou métodos da classe base subjacente, tal como faria normalmente.
Para esta classe, este método OnPointSizeChanged precisa definir a propriedade FontSize com base em o novo tamanho de ponto e um fator de conversão depende do dispositivo. Além disso, o construtor tem de iniciar o funcionamento da propriedade TamanhoDoTipoDeLetra com base no valor PointSize padrão. Isto é feito através de um método simples SetLabelFontSize. Aqui é a classe final completo, que utiliza as resoluções dependentes da plataforma discutidos no Capítulo 5, "Lidar com tamanhos":
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty = BindableProperty.Create("PointSize", // propertyName
typeof(double), // returnType typeof(AltLabel), // declaringType
8.0, // defaultValue
propertyChanged: OnPointSizeChanged);
public AltLabel()
{
SetLabelFontSize((double)PointSizeProperty.DefaultValue);
}
public double PointSize
{
set { SetValue(PointSizeProperty, value); }
get { return (double)GetValue(PointSizeProperty); }
}
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
((AltLabel)bindable).OnPointSizeChanged((double)oldValue, (double)newValue);
}
void OnPointSizeChanged(double oldValue, double newValue)
{
SetLabelFontSize(newValue);
}
void SetLabelFontSize(double pointSize)
{
FontSize = Device.OnPlatform(160, 160, 240) * pointSize / 72;
}
}
Também é possível para a propriedade instância OnPointSizeChanged para acessar a propriedade PointSize diretamente em vez de usar newValue. No momento em que o manipulador de propriedade alterada é chamado, o valor da propriedade subjacente já foi alterada. No entanto, você não tem acesso direto a esse valor subjacente, como você faz quando um campo privado apoia uma propriedade CLR. Esse valor subjacente é privado para BindableObject e só são acessíveis por meio da chamada GetValue.
Claro, nada impede que o código que está usando AltLabel de definir a propriedade FontSize e substituindo a configuração PointSize, mas vamos esperar que tal código é consciente disso. Aqui está algum código que é um programa chamado PointSizedText que usa AltLabel para exibir tamanhos em pontos de 4 a 12:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="PointSizedText.PointSizedTextPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness" iOS="5, 20, 0, 0" Android="5, 0, 0, 0" WinPhone="5, 0, 0, 0" />
</ContentPage.Padding>
<StackLayout x:Name="stackLayout">
<toolkit:AltLabel Text="Text of 4 points" PointSize="4" />
<toolkit:AltLabel Text="Text of 5 points" PointSize="5" />
<toolkit:AltLabel Text="Text of 6 points" PointSize="6" />
<toolkit:AltLabel Text="Text of 7 points" PointSize="7" />
<toolkit:AltLabel Text="Text of 8 points" PointSize="8" />
<toolkit:AltLabel Text="Text of 9 points" PointSize="9" />
<toolkit:AltLabel Text="Text of 10 points" PointSize="10" />
<toolkit:AltLabel Text="Text of 11 points" PointSize="11" />
<toolkit:AltLabel Text="Text of 12 points" PointSize="12" />
</StackLayout>
</ContentPage>
E aqui estão os screenshots: