@@ -1564,19 +1564,22 @@ protected static Locale findLocale() {
15641564 return locale ;
15651565 }
15661566
1567+ private void fireValidationEvents (
1568+ BindingValidationStatus <TARGET > status ) {
1569+ var statusChange = new BinderValidationStatus <>(getBinder (),
1570+ Collections .singletonList (status ), Collections .emptyList ());
1571+ getBinder ().getValidationStatusHandler ().statusChange (statusChange );
1572+ getBinder ().signalStatusChangeFromBinding (status );
1573+ getBinder ().fireStatusChangeEvent (status .isError ());
1574+ }
1575+
15671576 @ Override
15681577 public BindingValidationStatus <TARGET > validate (boolean fireEvent ) {
15691578 Objects .requireNonNull (binder ,
15701579 "This Binding is no longer attached to a Binder" );
15711580 BindingValidationStatus <TARGET > status = doValidation ();
15721581 if (fireEvent ) {
1573- var statusChange = new BinderValidationStatus <>(getBinder (),
1574- Collections .singletonList (status ),
1575- Collections .emptyList ());
1576- getBinder ().getValidationStatusHandler ()
1577- .statusChange (statusChange );
1578- getBinder ().signalStatusChangeFromBinding (status );
1579- getBinder ().fireStatusChangeEvent (status .isError ());
1582+ fireValidationEvents (status );
15801583 }
15811584 return status ;
15821585 }
@@ -1615,20 +1618,33 @@ public void unbind() {
16151618 }
16161619
16171620 /**
1618- * Returns the field value run through all converters and validators,
1619- * but doesn't pass the {@link BindingValidationStatus} to any status
1620- * handler.
1621+ * Runs the field value through all converters and validators without
1622+ * wrapping in {@code untracked()}. This allows signal dependency
1623+ * tracking when called from the reactive effect in
1624+ * {@link #initInternalSignalEffectForValidators()}.
16211625 *
16221626 * @return the result of the conversion
16231627 */
1624- private Result <TARGET > doConversion () {
1628+ private Result <TARGET > executeConversionChain () {
16251629 return execute (() -> {
16261630 FIELDVALUE fieldValue = field .getValue ();
16271631 return converterValidatorChain .convertToModel (fieldValue ,
16281632 createValueContext ());
16291633 });
16301634 }
16311635
1636+ /**
1637+ * Returns the field value run through all converters and validators,
1638+ * but doesn't pass the {@link BindingValidationStatus} to any status
1639+ * handler. Always runs inside {@code untracked()} so that callers
1640+ * outside a reactive context never trigger signal tracking.
1641+ *
1642+ * @return the result of the conversion
1643+ */
1644+ private Result <TARGET > doConversion () {
1645+ return UsageTracker .untracked (this ::executeConversionChain );
1646+ }
1647+
16321648 private BindingValidationStatus <TARGET > toValidationStatus (
16331649 Result <TARGET > result ) {
16341650 return new BindingValidationStatus <>(result , this );
@@ -1711,11 +1727,12 @@ private void initInternalSignalEffectForValidators() {
17111727 if (signalRegistration == null
17121728 && getField () instanceof Component component ) {
17131729 signalRegistration = Signal .effect (component , () -> {
1714- if (valueInit ) {
1715- // start to track signal usage
1716- doConversion ();
1717- } else {
1718- validate ();
1730+ // Run chain with real tracking to discover signal deps
1731+ Result <TARGET > result = executeConversionChain ();
1732+ if (!valueInit ) {
1733+ BindingValidationStatus <TARGET > status = toValidationStatus (
1734+ result );
1735+ fireValidationEvents (status );
17191736 }
17201737 });
17211738 }
@@ -1987,6 +2004,9 @@ public TARGET value() {
19872004 }
19882005
19892006 private void trackUsageOfInternalValidationSignal () {
2007+ if (!UsageTracker .isActive ()) {
2008+ return ;
2009+ }
19902010 if (internalValidationTriggerSignal == null ) {
19912011 internalValidationTriggerSignal = new ValueSignal <>(false );
19922012 }
0 commit comments