@@ -12,7 +12,6 @@ namespace Devolutions.AvaloniaControls.Helpers;
1212using Avalonia . Data ;
1313using Avalonia . Data . Converters ;
1414using Avalonia . LogicalTree ;
15- using Avalonia . Markup . Xaml . MarkupExtensions ;
1615
1716[ RequiresUnreferencedCode ( "BindingEvaluator require preserved types" ) ]
1817[ RequiresDynamicCode ( "BindingEvaluator require preserved types" ) ]
@@ -132,6 +131,11 @@ public BindingEvaluator(StyledElement anchor, Type dataContextType)
132131 return getter ;
133132 }
134133
134+ if ( this . TryBuildIntermediateRawGetter ( binding , out getter ) )
135+ {
136+ return getter ;
137+ }
138+
135139 return this . BuildProxyRawGetter ( binding ) ;
136140 }
137141
@@ -142,6 +146,11 @@ public BindingEvaluator(StyledElement anchor, Type dataContextType)
142146 return expression ;
143147 }
144148
149+ if ( TryBuildIntermediateRawExpression ( binding , out expression ) )
150+ {
151+ return expression ;
152+ }
153+
145154 return this . BuildProxyRawGetterExpression < TDataContext > ( binding ) ;
146155 }
147156
@@ -153,34 +162,29 @@ public BindingEvaluator(StyledElement anchor, Type dataContextType)
153162 }
154163
155164 ParameterExpression rowParameter = Expression . Parameter ( typeof ( TDataContext ) , "row" ) ;
156- Expression propertyAccess = rowParameter ;
165+ Expression propertyAccess ;
157166
158167 try
159168 {
160- foreach ( string propertyName in path . Split ( '.' ) )
161- {
162- propertyAccess = Expression . PropertyOrField ( propertyAccess , propertyName ) ;
163- }
169+ propertyAccess = BuildNullSafePropertyAccess ( rowParameter , path ) ;
164170 }
165171 catch
166172 {
167173 return null ;
168174 }
169175
170- if ( propertyAccess . Type == typeof ( string ) )
171- {
172- Expression body = Expression . Coalesce ( propertyAccess , Expression . Constant ( string . Empty ) ) ;
173- return Expression . Lambda < Func < TDataContext , string > > ( body , rowParameter ) ;
174- }
175-
176- Expression toStringExpr = ToStringExpression ( propertyAccess ) ;
177- Expression < Func < TDataContext , string > > innerLambda = Expression . Lambda < Func < TDataContext , string > > ( toStringExpr , rowParameter ) ;
178- Func < TDataContext , string ? > compiledGetter = innerLambda . Compile ( ) ;
179-
180- ParameterExpression outerParam = Expression . Parameter ( typeof ( TDataContext ) , "row" ) ;
181- Expression outerBody = Expression . Invoke ( Expression . Constant ( compiledGetter ) , outerParam ) ;
182-
183- return Expression . Lambda < Func < TDataContext , string > > ( outerBody , outerParam ) ;
176+ ParameterExpression value = Expression . Variable ( typeof ( object ) , "value" ) ;
177+ Expression missingValue = Expression . OrElse (
178+ Expression . ReferenceEqual ( value , Expression . Constant ( null ) ) ,
179+ Expression . ReferenceEqual ( value , Expression . Constant ( AvaloniaProperty . UnsetValue ) ) ) ;
180+ Expression valueAsString = Expression . Coalesce (
181+ Expression . Call ( value , objectToStringMethod ) ,
182+ Expression . Constant ( string . Empty ) ) ;
183+ Expression body = Expression . Block (
184+ [ value ] ,
185+ Expression . Assign ( value , propertyAccess ) ,
186+ Expression . Condition ( missingValue , Expression . Constant ( string . Empty ) , valueAsString ) ) ;
187+ return Expression . Lambda < Func < TDataContext , string > > ( body , rowParameter ) ;
184188 }
185189
186190 private static Expression < Func < TDataContext , object ? > > ? BuildFastPathRawExpression < TDataContext > ( string path )
@@ -191,40 +195,53 @@ public BindingEvaluator(StyledElement anchor, Type dataContextType)
191195 }
192196
193197 ParameterExpression rowParameter = Expression . Parameter ( typeof ( TDataContext ) , "row" ) ;
194- Expression propertyAccess = rowParameter ;
198+ Expression body ;
195199
196200 try
197201 {
198- foreach ( string propertyName in path . Split ( '.' ) )
199- {
200- propertyAccess = Expression . PropertyOrField ( propertyAccess , propertyName ) ;
201- }
202+ body = BuildNullSafePropertyAccess ( rowParameter , path ) ;
202203 }
203204 catch
204205 {
205206 return null ;
206207 }
207208
208- Expression body = propertyAccess . Type . IsValueType ? Expression . Convert ( propertyAccess , typeof ( object ) ) : propertyAccess ;
209209 return Expression . Lambda < Func < TDataContext , object ? > > ( body , rowParameter ) ;
210210 }
211211
212212 private static Func < object , object ? > BuildPropertyGetter ( string path , Type rootType )
213213 {
214214 ParameterExpression param = Expression . Parameter ( typeof ( object ) , "root" ) ;
215- Expression access = Expression . Convert ( param , rootType ) ;
215+ Expression access = BuildNullSafePropertyAccess ( Expression . Convert ( param , rootType ) , path ) ;
216216
217- foreach ( string propertyName in path . Split ( '.' ) )
218- {
219- access = Expression . PropertyOrField ( access , propertyName ) ;
220- }
217+ return Expression . Lambda < Func < object , object ? > > ( access , param ) . Compile ( ) ;
218+ }
221219
222- if ( access . Type . IsValueType )
220+ private static Expression BuildNullSafePropertyAccess ( Expression root , string path ) =>
221+ BuildNullSafePropertyAccess ( root , path . Split ( '.' ) , 0 ) ;
222+
223+ private static Expression BuildNullSafePropertyAccess ( Expression receiver , string [ ] propertyNames , int index )
224+ {
225+ while ( true )
223226 {
224- access = Expression . Convert ( access , typeof ( object ) ) ;
225- }
227+ if ( index == propertyNames . Length )
228+ {
229+ return Expression . Convert ( receiver , typeof ( object ) ) ;
230+ }
226231
227- return Expression . Lambda < Func < object , object ? > > ( access , param ) . Compile ( ) ;
232+ if ( receiver . Type . IsValueType && Nullable . GetUnderlyingType ( receiver . Type ) is null )
233+ {
234+ receiver = Expression . PropertyOrField ( receiver , propertyNames [ index ++ ] ) ;
235+ continue ;
236+ }
237+
238+ ParameterExpression receiverValue = Expression . Variable ( receiver . Type , $ "segment{ index } ") ;
239+ return Expression . Block ( [ receiverValue ] ,
240+ Expression . Assign ( receiverValue , receiver ) ,
241+ Expression . Condition ( Expression . Equal ( receiverValue , Expression . Constant ( null , receiver . Type ) ) ,
242+ Expression . Constant ( AvaloniaProperty . UnsetValue , typeof ( object ) ) ,
243+ BuildNullSafePropertyAccess ( Expression . PropertyOrField ( receiverValue , propertyNames [ index ] ) , propertyNames , index + 1 ) ) ) ;
244+ }
228245 }
229246
230247 private static StyledElement ? FindLogicalAncestorOfType ( StyledElement start , Type ancestorType )
@@ -266,11 +283,12 @@ public BindingEvaluator(StyledElement anchor, Type dataContextType)
266283 && b . Source == AvaloniaProperty . UnsetValue
267284 && string . IsNullOrEmpty ( b . ElementName )
268285 && b . RelativeSource is null => path ,
269- CompiledBindingExtension c when c . Converter is null
286+ CompiledBinding c when c . Converter is null
270287 && c . StringFormat is null
271288 && c . FallbackValue == AvaloniaProperty . UnsetValue
272289 && c . TargetNullValue == AvaloniaProperty . UnsetValue
273290 && c . Source == AvaloniaProperty . UnsetValue => c . Path ? . ToString ( ) ,
291+ // Note: `CompiledBindingExtension` IS a `CompiledBinding`
274292 _ => null ,
275293 } ;
276294
@@ -315,23 +333,6 @@ private static bool IsSimpleDotPath(string path)
315333 return null ;
316334 }
317335
318- private static Expression ToStringExpression ( Expression value )
319- {
320- if ( value . Type == typeof ( string ) )
321- {
322- return Expression . Coalesce ( value , Expression . Constant ( string . Empty ) ) ;
323- }
324-
325- if ( value . Type . IsValueType )
326- {
327- return Expression . Call ( value , objectToStringMethod ) ;
328- }
329-
330- Expression toString = Expression . Call ( value , objectToStringMethod ) ;
331- Expression toStringOrEmpty = Expression . Coalesce ( toString , Expression . Constant ( string . Empty ) ) ;
332- return Expression . Condition ( Expression . Equal ( value , Expression . Constant ( null , value . Type ) ) , Expression . Constant ( string . Empty ) , toStringOrEmpty ) ;
333- }
334-
335336 private static bool TryBuildFastPathRawExpression < TDataContext > ( BindingBase binding , [ NotNullWhen ( true ) ] out Expression < Func < TDataContext , object ? > > ? expression )
336337 {
337338 string ? pathString = GetSimplePathWithoutExtras ( binding ) ;
@@ -366,6 +367,120 @@ private bool TryBuildFastPathRawGetter(BindingBase binding, [NotNullWhen(true)]
366367 }
367368 }
368369
370+ private static bool TryBuildIntermediateRawExpression < TDataContext > (
371+ BindingBase binding ,
372+ [ NotNullWhen ( true ) ] out Expression < Func < TDataContext , object ? > > ? expression )
373+ {
374+ if ( ! TryGetIntermediateRawParts ( binding , out string ? path , out object ? fallbackValue , out object ? targetNullValue ) )
375+ {
376+ expression = null ;
377+ return false ;
378+ }
379+
380+ try
381+ {
382+ Func < object , object ? > getter = BuildIntermediateRawGetter ( path , fallbackValue , targetNullValue , typeof ( TDataContext ) ) ;
383+ expression = WrapRawObjectDelegateAsExpression < TDataContext > ( getter ) ;
384+ return true ;
385+ }
386+ catch
387+ {
388+ expression = null ;
389+ return false ;
390+ }
391+ }
392+
393+ private bool TryBuildIntermediateRawGetter ( BindingBase binding , [ NotNullWhen ( true ) ] out Func < object , object ? > ? getter )
394+ {
395+ if ( ! TryGetIntermediateRawParts ( binding , out string ? path , out object ? fallbackValue , out object ? targetNullValue ) )
396+ {
397+ getter = null ;
398+ return false ;
399+ }
400+
401+ try
402+ {
403+ getter = BuildIntermediateRawGetter ( path , fallbackValue , targetNullValue , this . dataContextType ) ;
404+ return true ;
405+ }
406+ catch
407+ {
408+ getter = null ;
409+ return false ;
410+ }
411+ }
412+
413+ private static Func < object , object ? > BuildIntermediateRawGetter (
414+ string path ,
415+ object ? fallbackValue ,
416+ object ? targetNullValue ,
417+ Type rowType )
418+ {
419+ Func < object , object ? > propertyGetter = BuildPropertyGetter ( path , rowType ) ;
420+
421+ return row =>
422+ {
423+ try
424+ {
425+ object ? value = propertyGetter ( row ) ;
426+ if ( ReferenceEquals ( value , AvaloniaProperty . UnsetValue ) )
427+ {
428+ return fallbackValue ;
429+ }
430+
431+ if ( value is null && ! ReferenceEquals ( targetNullValue , AvaloniaProperty . UnsetValue ) )
432+ {
433+ return targetNullValue ;
434+ }
435+
436+ return value ;
437+ }
438+ catch
439+ {
440+ return fallbackValue ;
441+ }
442+ } ;
443+ }
444+
445+ private static bool TryGetIntermediateRawParts (
446+ BindingBase binding ,
447+ [ NotNullWhen ( true ) ] out string ? path ,
448+ out object ? fallbackValue ,
449+ out object ? targetNullValue )
450+ {
451+ switch ( binding )
452+ {
453+ case Binding { Path : { Length : > 0 } bindingPath } b
454+ when b . Converter is null
455+ && b . StringFormat is null
456+ && b . Source == AvaloniaProperty . UnsetValue
457+ && string . IsNullOrEmpty ( b . ElementName )
458+ && b . RelativeSource is null :
459+ path = bindingPath ;
460+ fallbackValue = b . FallbackValue ;
461+ targetNullValue = b . TargetNullValue ;
462+ break ;
463+
464+ // Note: `CompiledBindingExtension` IS a `CompiledBinding`.
465+ case CompiledBinding c
466+ when c . Converter is null
467+ && c . StringFormat is null
468+ && c . Source == AvaloniaProperty . UnsetValue :
469+ path = c . Path ? . ToString ( ) ;
470+ fallbackValue = c . FallbackValue ;
471+ targetNullValue = c . TargetNullValue ;
472+ break ;
473+
474+ default :
475+ path = null ;
476+ fallbackValue = null ;
477+ targetNullValue = null ;
478+ return false ;
479+ }
480+
481+ return ! string . IsNullOrEmpty ( path ) && IsSimpleDotPath ( path ) ;
482+ }
483+
369484 private static bool TryBuildIntermediateExpression < TDataContext > ( BindingBase binding , [ NotNullWhen ( true ) ] out Expression < Func < TDataContext , string > > ? expression )
370485 {
371486 if ( ! TryGetIntermediateParts ( binding , out string ? path , out object ? source , out IValueConverter ? converter , out object ? converterParameter ,
@@ -416,6 +531,11 @@ private static Func<object, string> BuildIntermediateGetter(
416531 {
417532 object ? value = propertyGetter ( source ?? row ) ;
418533
534+ if ( value == AvaloniaProperty . UnsetValue )
535+ {
536+ return fallbackString ?? string . Empty ;
537+ }
538+
419539 if ( converter is not null )
420540 {
421541 value = converter . Convert ( value , typeof ( string ) , converterParameter , culture ) ;
@@ -464,7 +584,8 @@ private static bool TryGetIntermediateParts(
464584 targetNullValue = b . TargetNullValue == AvaloniaProperty . UnsetValue ? null : b . TargetNullValue ;
465585 return true ;
466586
467- case CompiledBindingExtension c :
587+ case CompiledBinding c :
588+ {
468589 string ? pathString = c . Path ? . ToString ( ) ;
469590 if ( string . IsNullOrEmpty ( pathString ) || ! IsSimpleDotPath ( pathString ) )
470591 {
@@ -480,6 +601,9 @@ private static bool TryGetIntermediateParts(
480601 fallbackValue = c . FallbackValue == AvaloniaProperty . UnsetValue ? null : c . FallbackValue ;
481602 targetNullValue = c . TargetNullValue == AvaloniaProperty . UnsetValue ? null : c . TargetNullValue ;
482603 return true ;
604+ }
605+
606+ // Note: `CompiledBindingExtension` IS a `CompiledBinding`
483607 }
484608
485609 path = null ;
@@ -521,7 +645,9 @@ private bool TryGetSimpleBindingGetter(BindingBase binding, [NotNullWhen(true)]
521645 try
522646 {
523647 Func < object , object ? > rawGetter = BuildPropertyGetter ( pathString , this . dataContextType ) ;
524- getter = row => rawGetter ( row ) is { } value ? value as string ?? value . ToString ( ) ?? string . Empty : string . Empty ;
648+ getter = row => rawGetter ( row ) is { } value && value != AvaloniaProperty . UnsetValue
649+ ? value as string ?? value . ToString ( ) ?? string . Empty
650+ : string . Empty ;
525651 return true ;
526652 }
527653 catch
@@ -538,6 +664,13 @@ private static Expression<Func<TDataContext, string>> WrapObjectDelegateAsExpres
538664 return Expression . Lambda < Func < TDataContext , string > > ( body , rowParameter ) ;
539665 }
540666
667+ private static Expression < Func < TDataContext , object ? > > WrapRawObjectDelegateAsExpression < TDataContext > ( Func < object , object ? > getter )
668+ {
669+ ParameterExpression rowParameter = Expression . Parameter ( typeof ( TDataContext ) , "row" ) ;
670+ Expression body = Expression . Invoke ( Expression . Constant ( getter ) , Expression . Convert ( rowParameter , typeof ( object ) ) ) ;
671+ return Expression . Lambda < Func < TDataContext , object ? > > ( body , rowParameter ) ;
672+ }
673+
541674 private Func < object , string > BuildFrameworkDelegatedGetter ( BindingBase binding )
542675 {
543676 binding = this . RewriteParentBindingIfNeeded ( binding ) ;
@@ -584,9 +717,7 @@ private Expression<Func<TDataContext, string>> BuildFrameworkDelegatedGetterExpr
584717 private Expression < Func < TDataContext , object ? > > BuildProxyRawGetterExpression < TDataContext > ( BindingBase binding )
585718 {
586719 Func < object , object ? > getter = this . BuildProxyRawGetter ( binding ) ;
587- ParameterExpression rowParameter = Expression . Parameter ( typeof ( TDataContext ) , "row" ) ;
588- Expression body = Expression . Invoke ( Expression . Constant ( getter ) , Expression . Convert ( rowParameter , typeof ( object ) ) ) ;
589- return Expression . Lambda < Func < TDataContext , object ? > > ( body , rowParameter ) ;
720+ return WrapRawObjectDelegateAsExpression < TDataContext > ( getter ) ;
590721 }
591722
592723 private ParentPathRewrite ? GetOrCreatePathRewrite ( string rawPath )
0 commit comments