@@ -1773,6 +1773,87 @@ public string Title
17731773 }
17741774 }
17751775
1776+ [ Fact ]
1777+ //https://github.com/dotnet/maui/issues/34428
1778+ public void TypedBinding_NestedProperty_ResubscribesAfterNullIntermediateBecomesNonNull ( )
1779+ {
1780+ // Regression: when an intermediate object in the path starts as null and later becomes
1781+ // non-null, the binding must re-establish subscriptions to nested properties.
1782+ // Previously, the _isSubscribed flag prevented re-subscribing after the first Apply.
1783+
1784+ var vm = new ComplexMockViewModel
1785+ {
1786+ Model = null // Start with null intermediate
1787+ } ;
1788+
1789+ var property = BindableProperty . Create ( "Text" , typeof ( string ) , typeof ( MockBindable ) , null ) ;
1790+
1791+ var binding = new TypedBinding < ComplexMockViewModel , string > (
1792+ cvm => cvm . Model is { } m ? ( m . Text , true ) : ( null , false ) ,
1793+ ( cvm , t ) => { if ( cvm . Model is { } m ) m . Text = t ; } ,
1794+ new [ ] {
1795+ new Tuple < Func < ComplexMockViewModel , object > , string > ( cvm => cvm , "Model" ) ,
1796+ new Tuple < Func < ComplexMockViewModel , object > , string > ( cvm => cvm . Model , "Text" )
1797+ } )
1798+ { Mode = BindingMode . OneWay } ;
1799+
1800+ var bindable = new MockBindable ( ) ;
1801+ bindable . SetBinding ( property , binding ) ;
1802+ bindable . BindingContext = vm ;
1803+
1804+ // Initially null model → binding returns null/default
1805+ Assert . Null ( bindable . GetValue ( property ) ) ;
1806+
1807+ // Set Model to non-null → binding should pick up the value
1808+ vm . Model = new ComplexMockViewModel { Text = "Initial" } ;
1809+ Assert . Equal ( "Initial" , ( string ) bindable . GetValue ( property ) ) ;
1810+
1811+ // Change nested property → binding MUST update (this was the regression)
1812+ vm . Model . Text = "Updated" ;
1813+ Assert . Equal ( "Updated" , ( string ) bindable . GetValue ( property ) ) ;
1814+ }
1815+
1816+ [ Fact ]
1817+ //https://github.com/dotnet/maui/issues/34428
1818+ public void TypedBinding_NestedProperty_ResubscribesAfterIntermediateReplaced ( )
1819+ {
1820+ // When the intermediate object is replaced (non-null → different non-null object),
1821+ // the binding must switch subscriptions to the new object.
1822+
1823+ var child1 = new ComplexMockViewModel { Text = "Child1" } ;
1824+ var child2 = new ComplexMockViewModel { Text = "Child2" } ;
1825+ var vm = new ComplexMockViewModel { Model = child1 } ;
1826+
1827+ var property = BindableProperty . Create ( "Text" , typeof ( string ) , typeof ( MockBindable ) , null ) ;
1828+
1829+ var binding = new TypedBinding < ComplexMockViewModel , string > (
1830+ cvm => cvm . Model is { } m ? ( m . Text , true ) : ( null , false ) ,
1831+ ( cvm , t ) => { if ( cvm . Model is { } m ) m . Text = t ; } ,
1832+ new [ ] {
1833+ new Tuple < Func < ComplexMockViewModel , object > , string > ( cvm => cvm , "Model" ) ,
1834+ new Tuple < Func < ComplexMockViewModel , object > , string > ( cvm => cvm . Model , "Text" )
1835+ } )
1836+ { Mode = BindingMode . OneWay } ;
1837+
1838+ var bindable = new MockBindable ( ) ;
1839+ bindable . SetBinding ( property , binding ) ;
1840+ bindable . BindingContext = vm ;
1841+
1842+ Assert . Equal ( "Child1" , ( string ) bindable . GetValue ( property ) ) ;
1843+
1844+ // Replace intermediate with a different object
1845+ vm . Model = child2 ;
1846+ Assert . Equal ( "Child2" , ( string ) bindable . GetValue ( property ) ) ;
1847+
1848+ // Changing the OLD intermediate should NOT fire the binding
1849+ child1 . Text = "OldChildChanged" ;
1850+ Assert . Equal ( "Child2" , ( string ) bindable . GetValue ( property ) ) ;
1851+
1852+ // Changing the NEW intermediate SHOULD fire the binding
1853+ child2 . Text = "Child2Updated" ;
1854+ Assert . Equal ( "Child2Updated" , ( string ) bindable . GetValue ( property ) ) ;
1855+ }
1856+
17761857 [ Fact ]
17771858 //https://github.com/xamarin/Microsoft.Maui.Controls/issues/3650
17781859 //https://github.com/xamarin/Microsoft.Maui.Controls/issues/3613
0 commit comments