Skip to content

Commit 67306e0

Browse files
committed
Compiled lambda expression, instead of the GetValue reflection method
Signed-off-by: Daniël Trommel <[email protected]>
1 parent 38462ab commit 67306e0

File tree

2 files changed

+77
-121
lines changed

2 files changed

+77
-121
lines changed

src/Columns/TableViewBoundColumn.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.UI.Xaml;
22
using Microsoft.UI.Xaml.Data;
33
using System;
4+
using System.Linq.Expressions;
45
using System.Reflection;
56
using WinUI.TableView.Extensions;
67

@@ -11,24 +12,21 @@ namespace WinUI.TableView;
1112
/// </summary>
1213
public abstract class TableViewBoundColumn : TableViewColumn
1314
{
14-
private Type? _listType;
1515
private string? _propertyPath;
1616
private Binding _binding = new();
17-
private (PropertyInfo, object?)[]? _propertyInfo;
17+
18+
private Func<object, object?>? _funcCompiledPropertyPath;
1819

1920
public override object? GetCellContent(object? dataItem)
2021
{
21-
if (dataItem is null) return null;
22+
if (dataItem is null)
23+
return null;
2224

23-
if (_propertyInfo is null || dataItem.GetType() != _listType)
24-
{
25-
_listType = dataItem.GetType();
26-
dataItem = dataItem.GetValue(_listType, PropertyPath, out _propertyInfo);
27-
}
28-
else
29-
{
30-
dataItem = dataItem.GetValue(_propertyInfo);
31-
}
25+
if (_funcCompiledPropertyPath is null && !string.IsNullOrWhiteSpace(PropertyPath))
26+
_funcCompiledPropertyPath = dataItem.GetFuncCompiledPropertyPath(PropertyPath!);
27+
28+
if (_funcCompiledPropertyPath is not null)
29+
dataItem = _funcCompiledPropertyPath(dataItem);
3230

3331
if (Binding?.Converter is not null)
3432
{

src/Extensions/ObjectExtensions.cs

Lines changed: 67 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using Microsoft.UI.Xaml;
12
using System;
23
using System.Collections;
34
using System.Linq;
5+
using System.Linq.Expressions;
46
using System.Reflection;
57
using System.Text.RegularExpressions;
68

@@ -16,141 +18,97 @@ internal static partial class ObjectExtensions
1618
private static partial Regex PropertyPathRegex();
1719

1820
/// <summary>
19-
/// Gets the value of a property from an object using a sequence of property info and index pairs.
21+
/// Creates and returns a compiled lambda expression for accessing the property path on instances, with runtime type checking and casting support.
2022
/// </summary>
21-
/// <param name="obj">The object from which to get the value.</param>
22-
/// <param name="pis">An array of property info and index pairs.</param>
23-
/// <returns>The value of the property, or null if the object is null.</returns>
24-
internal static object? GetValue(this object? obj, (PropertyInfo pi, object? index)[] pis)
23+
/// <param name="dataItem">The data item instance to use for runtime type evaluation.</param>
24+
/// <param name="bindingPath">The binding path to access, e.g. "[0].SubPropertyArray[0].SubSubProperty".</param>
25+
/// <returns>A compiled function that takes an instance and returns the property value, or null if the property path is invalid.</returns>
26+
public static Func<object, object?>? GetFuncCompiledPropertyPath(this object dataItem, string bindingPath)
2527
{
26-
foreach (var (pi, index) in pis)
28+
try
2729
{
28-
if (obj is null)
29-
break;
30-
31-
if (pi != null)
32-
{
33-
// Use property getter, with or without index
34-
obj = index is not null ? pi.GetValue(obj, [index]) : pi.GetValue(obj);
35-
}
36-
else if (index is int i)
37-
{
38-
// Array
39-
if (obj is Array arr)
40-
{
41-
obj = arr.GetValue(i);
42-
}
43-
// IList
44-
else if (obj is IList list)
45-
{
46-
obj = list[i];
47-
}
48-
else
49-
{
50-
// Not a supported indexer type
51-
return null;
52-
}
53-
}
54-
else
55-
{
56-
// Not a supported path segment
57-
return null;
58-
}
30+
// Build the property access expression chain with runtime type checking
31+
var parameterObj = Expression.Parameter(typeof(object), "obj"); ;
32+
var propertyAccess = BuildPropertyPathExpression(parameterObj, bindingPath, dataItem);
33+
34+
// Compile the lambda expression
35+
var lambda = Expression.Lambda<Func<object, object?>>(propertyAccess, parameterObj);
36+
var _compiledPropertyPath = lambda.Compile();
37+
return _compiledPropertyPath;
38+
}
39+
catch
40+
{
41+
// If compilation fails, fall back to reflection-based approach
42+
return null;
5943
}
60-
61-
return obj;
6244
}
6345

6446
/// <summary>
65-
/// Gets the value of a property from an object using a type and a property path.
47+
/// Builds an expression tree for accessing a property path on the given instance expression, with runtime type checking and casting support.
6648
/// </summary>
67-
/// <param name="obj">The object from which to get the value.</param>
68-
/// <param name="type">The type of the object.</param>
69-
/// <param name="path">The property path.</param>
70-
/// <param name="pis">An array of property info and index pairs.</param>
71-
/// <returns>The value of the property, or null if the object is null.</returns>
72-
internal static object? GetValue(this object? obj, Type? type, string? path, out (PropertyInfo pi, object? index)[] pis)
49+
/// <param name="parameterObj">The expression representing the instance parameter for which the binding path will be evaluated.</param>
50+
/// <param name="bindingPath">The binding path to access.</param>
51+
/// <param name="dataItem">The actual data item to use for runtime type evaluation, to help with any needed subclass type conversions.</param>
52+
/// <returns>An expression that accesses the binding path from the </returns>
53+
private static Expression BuildPropertyPathExpression(ParameterExpression parameterObj, string bindingPath, object dataItem)
7354
{
74-
if (obj == null || string.IsNullOrWhiteSpace(path) || type == null)
75-
{
76-
pis = [];
77-
return obj;
78-
}
55+
Expression current = parameterObj;
7956

80-
var matches = PropertyPathRegex().Matches(path);
81-
if (matches.Count == 0)
82-
{
83-
pis = [];
84-
return obj;
85-
}
57+
// The function uses a generic object input parameter to allow for any type of data item,
58+
// but we need to ensure that the runtime type matches the data item type that is inputted as example to be able to find members
59+
if (current.Type != dataItem.GetType())
60+
current = Expression.Convert(current, dataItem.GetType());
8661

87-
// Pre-size the steps array to the number of matches
88-
pis = new (PropertyInfo, object?)[matches.Count];
89-
int i = 0;
90-
object? current = obj;
91-
Type? currentType = type;
62+
var matches = PropertyPathRegex().Matches(bindingPath);
9263

9364
foreach (Match match in matches)
9465
{
9566
string part = match.Value;
96-
object? index = null;
97-
PropertyInfo? pi = null;
9867

68+
// Indexer
9969
if (part.StartsWith('[') && part.EndsWith(']'))
10070
{
101-
// Indexer: [int] or [string]
102-
string indexer = part[1..^1];
103-
if (int.TryParse(indexer, out int intIndex))
104-
index = intIndex;
105-
else
106-
index = indexer;
71+
string stringIndex = part[1..^1];
10772

108-
// Try array
109-
if (current is Array arr && index is int idx)
73+
// See if this is an integer index
74+
if (int.TryParse(stringIndex, out int index))
11075
{
111-
current = arr.GetValue(idx);
112-
pis[i++] = (null!, idx);
113-
currentType = current?.GetType();
114-
continue;
76+
if (current.Type.IsArray)
77+
{
78+
current = Expression.ArrayIndex(current, Expression.Constant(index));
79+
}
80+
else
81+
{
82+
// Try to find an indexer property, with an int parameter
83+
var indexerProperty = current.Type.GetProperty("Item", new[] { typeof(int) })
84+
?? throw new ArgumentException($"Type '{current.Type.Name}' does not support integer indexing");
85+
current = Expression.Property(current, indexerProperty, Expression.Constant(index));
86+
}
11587
}
116-
117-
// Try IList
118-
if (current is IList list && index is int idx2)
119-
{
120-
current = list[idx2];
121-
pis[i++] = (null!, idx2);
122-
currentType = current?.GetType();
123-
continue;
124-
}
125-
126-
// Try to find a default indexer property "Item" (e.g., this[string]);
127-
// Note that only single argument indexers of type int or string are currently support
128-
pi = currentType?.GetProperty("Item", [index.GetType()]);
129-
if (pi != null)
88+
else
13089
{
131-
current = pi.GetValue(current, [index]);
132-
pis[i++] = (pi, index);
133-
currentType = current?.GetType();
134-
continue;
90+
// Try to find an indexer property, with an string parameter
91+
var indexerProperty = current.Type.GetProperty("Item", new[] { typeof(string) })
92+
?? throw new ArgumentException($"Type '{current.Type.Name}' does not support string indexing");
93+
current = Expression.Property(current, indexerProperty, Expression.Constant(stringIndex));
13594
}
136-
137-
// Not found
138-
pis = null!;
139-
return null;
14095
}
96+
// Simple property access
14197
else
142-
{
143-
// Property access
144-
pi = currentType?.GetProperty(part, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
145-
if (pi == null)
146-
{
147-
pis = null!;
148-
return null;
149-
}
150-
current = pi.GetValue(current);
151-
pis[i++] = (pi, null);
152-
currentType = current?.GetType();
98+
{
99+
var propertyInfo = current.Type.GetProperty(part, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
100+
?? throw new ArgumentException($"Property '{part}' not found on type '{current.Type.Name}'");
101+
current = Expression.Property(current, propertyInfo);
153102
}
103+
104+
// Compile a lambda of the partial expression thus far, to see if we need to add a cast
105+
var lambdaTemp = Expression.Lambda<Func<object, object?>>(current, parameterObj);
106+
var funcCurrent = lambdaTemp.Compile();
107+
// Evaluate this compiled function, to see if the result type is more specific than the current expression type. If so, cast to it
108+
var result = funcCurrent(dataItem);
109+
var runtimeType = result?.GetType() ?? current.Type;
110+
if (current.Type != runtimeType && current.Type.IsAssignableFrom(runtimeType))
111+
current = Expression.Convert(current, runtimeType);
154112
}
155113

156114
return current;

0 commit comments

Comments
 (0)