您想提供一组表达式并使用每个表达式在网格中创建自己的列。您必须解决一个主要问题:使用数据绑定到对象的属性来解析 DataGrid 列:
<DataGridTextColumn Header="ProcessName" Binding="{Binding ProcessName}" />
因此我们可以使用适当的绑定将属性访问表达式映射到 DataGrid 列。但是您的第二列并不代表属性访问,而是方法调用;并且您不能将 datagridcolumn 绑定到方法调用:
<!-- won't work -->
<DataGridTextColumn Header="GetSafeFilenamee" Binding="{Binding GetSafeFilename}" />
在这种情况下,因为该方法的目的是处理尝试访问MainModule 上的详细信息时可能出现的异常;我们也许可以通过属性访问和使用 WPF 的目标回退机制来避免异常。但是,一个可以进入任意方法的 IL 以找出相关属性访问的通用机制几乎肯定超出了您想要做的范围。
我建议使用包含多个属性访问的单个表达式,而不是 PopulateDataGrid 采用多个表达式,每个表达式都有自己的属性访问。我能想到两种这样的表达方式:
-
返回某个数组的表达式
PopulateDataGrid(processes, x => new [] { x.ProcessName, x.MainModule.FileName });
-
或返回匿名类型的表达式。这有一个额外的好处是允许您将标题传递给列:
PopulateDataGrid(processes, x => new { x.ProcessName, Path = x.MainModule.FileName });
另外,我建议将其作为 DataGrid 上的扩展方法公开。签名可能如下所示:
public static void PopulateDataGrid<TElement, TFieldsExpression>(this DataGrid dg, IEnumerable<TElement> itemsSource, Expression<Func<TElement, TFieldsExpression>> fieldsExpr) {
}
我们需要TFieldsExpression 泛型参数,以便编译器可以将第二个参数识别为表达式。
第一步是将多表达式解析为单独的标头和属性访问。您可以使用以下内容:
private static List<(string name, Expression expr)> ParseFields<TElement, TFieldsExpression>(Expression<Func<TElement, TFieldsExpression>> fieldsExpression) {
var body = fieldsExpression.Body;
switch (body) {
// an array initialization with elements
// (as opposed to an array initialization with bounds -- new int[5])
case NewArrayExpression newArrayExpr when body.NodeType == ExpressionType.NewArrayInit:
return newArrayExpr.Expressions.Select(x => ("", x)).ToList();
// anonymous type
// the IsAnonymous extension method is included at the end of the post
case NewExpression newExpr when newExpr.Type.IsAnonymous():
return newExpr.Constructor.GetParameters().Select(x => x.Name).Zip(newExpr.Arguments).ToList();
default:
throw new ArgumentException("Unhandled expression type.");
}
}
那么你可以编写如下方法:
public static void PopulateDataGrid<TElement, TFieldsExpression>(this DataGrid dg, IEnumerable<TElement> itemsSource, Expression<Func<TElement, TFieldsExpression>> fieldsExpr) {
dg.ItemsSource = itemsSource;
dg.Columns.Clear();
var fields = ParseFields(fieldsExpr);
foreach (var (name, expr) in fields) {
if (expr is MemberAccessExpression mexpr) {
dg.Columns.Add(new DataGridTextColumn {
Header = name,
Binding = new Binding(mexpr.Member.Name)
})
} else {
throw new ArgumentException("Unhandled expression type.");
}
}
}
然后您可以按如下方式调用此方法:
dg.PopulateDataGrid(list, x => new [] {x.ProcessName, x.HasExited, x.MachineName};
注意:其中大部分来自sample code,伴随着MSDN article I've written about expression trees。示例代码处理额外的表达式类型、长路径链(例如x.MainModule.FileName)和对String.Format的方法调用。
助手扩展:
// using System.Reflection;
public static bool IsAnonymous(this Type type) =>
type.HasAttribute<CompilerGeneratedAttribute>() && type.Name.Contains("Anonymous") && type.Name.ContainsAny("<>", "VB$");
public static bool HasAttribute<TAttribute>(this MemberInfo mi, bool inherit = false) where TAttribute : Attribute =>
mi.GetCustomAttributes(typeof(TAttribute), inherit).Any();
public static bool ContainsAny(this string s, params string[] testStrings) =>
testStrings.Any(x => s.Contains(x));