Skip to content

Commit 8829032

Browse files
authored
Fixed BindingEvaluator to properly support CompiledBinding in Avalonia 12 CR:xfortin (#623)
1 parent 6772537 commit 8829032

2 files changed

Lines changed: 423 additions & 58 deletions

File tree

src/Devolutions.AvaloniaControls/Helpers/BindingEvaluator.cs

Lines changed: 189 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace Devolutions.AvaloniaControls.Helpers;
1212
using Avalonia.Data;
1313
using Avalonia.Data.Converters;
1414
using 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

Comments
 (0)