Eu estava escrevendo uma classe em javafx onde eu tinha duas propriedades que eram vinculadas bidirecionalmente e estava testando alguns casos extremos. Descobri que se você alterasse o valor de uma das propriedades de dentro de um ouvinte de invalidação, isso fazia com que as propriedades ficassem fora de sincronia. Aqui está um pequeno exemplo:
static class A {
IntegerProperty x = new SimpleIntegerProperty(this, "x", 0);
IntegerProperty y = new SimpleIntegerProperty(this, "y", 0);
A() {
x.bindBidirectional(y);
}
}
static void testA() {
A a = new A();
a.x.addListener( o -> { //InvalidationListener
if (a.x.get() < 0) a.x.set(0);
});
// after y is set to a negative value, x and y hold different values
// until either y is set to a value that's >= 0 or x is set to any value
a.y.set(-2);
System.out.println(a.x.get());
System.out.println(a.y.get());
}
Saída:
0
-2
Eu estava assumindo que ao usar uma ligação bidirecional, alterar uma propriedade sempre faria com que a outra fosse atualizada. Parece raro (e possivelmente imprudente) que alguém escrevesse um ouvinte de invalidação como esse, mas estou pensando defensivamente aqui. Se pelo menos uma dessas propriedades fosse exposta, não quero que seja possível quebrar nenhuma invariante da minha classe. Eu estava pensando que há três explicações possíveis aqui:
O contrato em ligações bidirecionais não é que elas sempre estejam em sincronia (ou elas mantêm o mesmo valor ou são marcadas como inválidas), é apenas em uma base de melhor esforço. Assim, invariantes de classe não devem ser baseadas nesse fato.
Alterar o valor dentro de um ouvinte de invalidação quebra a pré-condição de vínculos bidirecionais e deve ser evitado. Caso contrário, eles estão sempre em sincronia. Assim, você pode basear uma invariante de classe nesse fato, já que é razoável exigir que os clientes não escrevam ouvintes de invalidação assim.
É um bug e eles realmente devem ser sincronizados, não importa o que aconteça. Acho que se isso fosse verdade, você poderia facilmente produzir um loop infinito de notificações de alteração (como adicionar um ouvinte de invalidação a y que sempre define o valor como < 0). Então, caberia ao cliente evitar tais casos.
Alguma dessas explicações está próxima da verdade ou estou esquecendo de algo mais aqui? Além disso, eu gostaria de saber se existe algum outro tipo de operação bind que leve esses tipos de situações em consideração.
Sugiro que sua segunda teoria seja verdadeira:
Operação de ligação bidirecional
Para entender a operação de ligação bidirecional, você pode consultar a fonte.
Veja, por exemplo, TypedNumberBidirectionBinding . Ele tem um sinalizador
updating
que define e verifica quando uma propriedade é invalidada. Se a propriedade for atualizada novamente dentro do ouvinte de invalidação, o sinalizador de atualização será verdadeiro e a sincronização do valor da propriedade será ignorada.(código de invalidação simplificado para fins de exemplo).
Recomendação
O javadoc InvalidationListener afirma:
Portanto, recomendo que você não "altere o valor de uma das propriedades de dentro de um ouvinte de invalidação".
O caso do controle deslizante
Isso ocorre porque a classe Slider faz o que não é recomendado. Ela modifica o valor da propriedade value do slider em um listener de invalidação .
A modificação é fixar o valor da alteração no intervalo do controle deslizante.
Isso leva ao caso de uma vinculação bidirecional na valueProperty da classe Slider não sincronizar corretamente quando estiver fora dos limites.
A documentação do controle deslizante afirma :
Como projetar em torno disto
Conforme sugerido nesta resposta :
Em outras partes da API JavaFX, para casos um tanto semelhantes, a abordagem com conjuntos de propriedades gêmeas é usada, por exemplo, a propriedade node read/write disable e a propriedade read-only disabled , ou o uso da propriedade read-only na classe Window , como as propriedades output scale read-only e render scale read/write.
Estou marcando a resposta de jewelsea como correta, ela esclarece as coisas muito bem, eu acho. Eu só queria acrescentar que para o meu caso em particular, já que eu queria que meus binds sempre se mantivessem, a solução foi não expor minha propriedade bound diretamente, mas sim usar um
ReadOnlyObjectWrapper
para tornar minha propriedade somente leitura fora da minha classe. Já que propriedades somente leitura não podem ser definidas, você não pode anexar o tipo de ouvinte de invalidação que quebra binds a elas. Você ainda pode permitir modificações, por meio de um setter personalizado ou uma segunda propriedade, se necessário.Eu acho (com o benefício da retrospectiva) que o Slider deveria ter sido implementado usando uma abordagem similar. Por exemplo:
Agora, todos os binds funcionarão corretamente para
requestedValue
, enquantovalue
não pode ser vinculado diretamente. Isso também torna o código mais autodocumentado - o valor solicitado não é o mesmo que o valor real. Observe que não é necessário ter um min/max solicitado, pois eles realmente não podem ser vetados, em vez disso, alterar por exemplomin
pode fazer com quemax
evalue
também sejam alterados.