如何在 WPF DataGrid 中动态生成列?
Posted
技术标签:
【中文标题】如何在 WPF DataGrid 中动态生成列?【英文标题】:How do I dynamically generate columns in a WPF DataGrid? 【发布时间】:2010-12-31 06:44:36 【问题描述】:我正在尝试在 WPF 数据网格中显示查询结果。我绑定的 ItemsSource 类型是IEnumerable<dynamic>
。由于返回的字段直到运行时才确定,所以在评估查询之前我不知道数据的类型。每个“行”都以 ExpandoObject
的形式返回,其中包含表示字段的动态属性。
我希望AutoGenerateColumns
(如下所示)能够像使用静态类型一样从ExpandoObject
生成列,但它似乎没有。
<DataGrid AutoGenerateColumns="True" ItemsSource="Binding Results"/>
无论如何要以声明方式执行此操作,还是我必须用一些 C# 命令性地挂钩?
编辑
好的,这将为我提供正确的列:
// ExpandoObject implements IDictionary<string,object>
IEnumerable<IDictionary<string, object>> rows = dataGrid1.ItemsSource.OfType<IDictionary<string, object>>();
IEnumerable<string> columns = rows.SelectMany(d => d.Keys).Distinct(StringComparer.OrdinalIgnoreCase);
foreach (string s in columns)
dataGrid1.Columns.Add(new DataGridTextColumn Header = s );
所以现在只需要弄清楚如何将列绑定到 IDictionary 值。
【问题讨论】:
【参考方案1】:这里的问题是 clr 将为 ExpandoObject 本身创建列 - 但不能保证一组 ExpandoObject 在彼此之间共享相同的属性,没有规则让引擎知道需要创建哪些列。
也许像 Linq 匿名类型这样的东西更适合你。我不知道您使用的是哪种数据网格,但所有这些数据网格的绑定应该相同。这是 Telerik 数据网格的简单示例。link to telerik forums
这实际上并不是真正的动态,需要在编译时知道类型 - 但这是在运行时设置此类内容的简单方法。
如果您真的不知道要显示什么样的字段,问题就会变得更加棘手。可能的解决方案是:
使用 Reflection.Emit 在运行时创建类型映射,我认为可以创建一个通用值转换器来接受您的查询结果、创建一个新类型(并维护一个缓存列表)并返回一个对象列表.创建新的动态类型将遵循与您已经用于创建 ExpandoObjects 相同的算法MSDN on Reflection.EmitAn old but useful article on codeproject 使用 Dynamic Linq - 这可能是更简单、更快捷的方法。Using Dynamic Linq Getting around anonymous type headaches with dynamic linq使用动态 linq,您可以在运行时使用字符串创建匿名类型 - 您可以从查询结果中组合起来。第二个链接的示例用法:
var orders = db.Orders.Where("OrderDate > @0", DateTime.Now.AddDays(-30)).Select("new(OrderID, OrderDate)");
无论如何,基本思想是以某种方式将 itemgrid 设置为对象的集合,这些对象的 shared 公共属性可以通过反射找到。
【讨论】:
问题数据来自mp3文件中的标签,所以设置确实不一致。确实没有关于它们将是什么的编译时知识。我可以解决属性一致性问题,不幸的是 ExpandoObject 对反射不透明(尽管我可以看到这是一个难以解决的问题)。 在这种情况下,动态 linq 可能会有所帮助,但您可能需要两遍方法。解析一次数据以查看遇到哪些标签,然后再解析一次以填充新对象列表。我想问题是,如果任何 mp3 文件具有已定义的属性,那么在将值映射到对象(动态或非动态)之后,它们都必须具有该属性。【参考方案2】:我的回答来自Dynamic column binding in Xaml
我使用了一种遵循此伪代码模式的方法
columns = New DynamicTypeColumnList()
columns.Add(New DynamicTypeColumn("Name", GetType(String)))
dynamicType = DynamicTypeHelper.GetDynamicType(columns)
DynamicTypeHelper.GetDynamicType() 生成具有简单属性的类型。有关如何生成此类类型的详细信息,请参阅this post
然后要实际使用该类型,请执行以下操作
Dim rows as List(Of DynamicItem)
Dim row As DynamicItem = CType(Activator.CreateInstance(dynamicType), DynamicItem)
row("Name") = "Foo"
rows.Add(row)
dataGrid.DataContext = rows
【讨论】:
有趣的方法。我可能不得不做类似的事情,但想避免 Emit 部分。同时使用 Expando 和 Emitted 类型似乎是多余的。感谢您的链接;它给了我一些想法。【参考方案3】:最终我需要做两件事:
-
从查询返回的属性列表中手动生成列
设置 DataBinding 对象
在那之后,内置数据绑定启动并运行良好,似乎没有任何问题从ExpandoObject
中获取属性值。
<DataGrid AutoGenerateColumns="False" ItemsSource="Binding Results" />
和
// Since there is no guarantee that all the ExpandoObjects have the
// same set of properties, get the complete list of distinct property names
// - this represents the list of columns
var rows = dataGrid1.ItemsSource.OfType<IDictionary<string, object>>();
var columns = rows.SelectMany(d => d.Keys).Distinct(StringComparer.OrdinalIgnoreCase);
foreach (string text in columns)
// now set up a column and binding for each property
var column = new DataGridTextColumn
Header = text,
Binding = new Binding(text)
;
dataGrid1.Columns.Add(column);
【讨论】:
这很好用,但是你什么时候执行这段代码呢?当您在 DataContextChanged 上处理此问题时,尚未设置 ItemsSource 在我的实例中,ItemSource 绑定到名为 Results 的 ViewModel 属性。我在视图中有一个 INotifyPrpertyChanged 处理程序,该处理程序对该属性更改作出反应。 这是我的方法,但我偶然发现了一个问题。行验证呢?您是否必须处理 ExpandoObjects 上的行验证? @Ninglin 我不需要为我的用例做行验证【参考方案4】:虽然 OP 接受了答案,但它使用 AutoGenerateColumns="False"
,这与原始问题所要求的不完全相同。幸运的是,它也可以通过自动生成的列来解决。解决方案的关键是DynamicObject
,它可以同时具有静态和动态属性:
public class MyObject : DynamicObject, ICustomTypeDescriptor
// The object can have "normal", usual properties if you need them:
public string Property1 get; set;
public int Property2 get; set;
public MyObject()
public override IEnumerable<string> GetDynamicMemberNames()
// in addition to the "normal" properties above,
// the object can have some dynamically generated properties
// whose list we return here:
return list_of_dynamic_property_names;
public override bool TryGetMember(GetMemberBinder binder, out object result)
// for each dynamic property, we need to look up the actual value when asked:
if (<binder.Name is a correct name for your dynamic property>)
result = <whatever data binder.Name means>
return true;
else
result = null;
return false;
public override bool TrySetMember(SetMemberBinder binder, object value)
// for each dynamic property, we need to store the actual value when asked:
if (<binder.Name is a correct name for your dynamic property>)
<whatever storage binder.Name means> = value;
return true;
else
return false;
public PropertyDescriptorCollection GetProperties()
// This is where we assemble *all* properties:
var collection = new List<PropertyDescriptor>();
// here, we list all "standard" properties first:
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(this, true))
collection.Add(property);
// and dynamic ones second:
foreach (string name in GetDynamicMemberNames())
collection.Add(new CustomPropertyDescriptor(name, typeof(property_type), typeof(MyObject)));
return new PropertyDescriptorCollection(collection.ToArray());
public PropertyDescriptorCollection GetProperties(Attribute[] attributes) => TypeDescriptor.GetProperties(this, attributes, true);
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string GetClassName() => TypeDescriptor.GetClassName(this, true);
public string GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[] attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object GetPropertyOwner(PropertyDescriptor pd) => this;
对于ICustomTypeDescriptor
的实现,你可以大体上使用TypeDescriptor
的静态函数。 GetProperties()
是需要真正实现的:读取现有属性并添加动态属性。
PropertyDescriptor
是抽象的,你必须继承它:
public class CustomPropertyDescriptor : PropertyDescriptor
private Type componentType;
public CustomPropertyDescriptor(string propertyName, Type componentType)
: base(propertyName, new Attribute[] )
this.componentType = componentType;
public CustomPropertyDescriptor(string propertyName, Type componentType, Attribute[] attrs)
: base(propertyName, attrs)
this.componentType = componentType;
public override bool IsReadOnly => false;
public override Type ComponentType => componentType;
public override Type PropertyType => typeof(property_type);
public override bool CanResetValue(object component) => true;
public override void ResetValue(object component) => SetValue(component, null);
public override bool ShouldSerializeValue(object component) => true;
public override object GetValue(object component)
return ...;
public override void SetValue(object component, object value)
...
【讨论】:
在将ItemsSource
绑定到ObservableCollection<MyObject>
时,这似乎对我不起作用
这是缺失的部分:reimers.dk/jacob-reimers-blog/…
那个链接现在失效了;有人可以在这里发布完整的答案吗?
我多年来一直在程序中使用上述方案。
如果不需要它们可以忽略它们(在我提到的答案中“它们可以同时具有静态和动态属性”)。在我的实际情况中,但是 YMMV,我有一个对象,它既有固定列(如项目名称)和动态列(如不同数量括号的价格,例如 1 件、2-10 件、11-50 件,等等)。因此,如果您需要它们加上一组动态生成的动态属性,您可以拥有任意数量的“真实”属性。我将添加一些 cmets。以上是关于如何在 WPF DataGrid 中动态生成列?的主要内容,如果未能解决你的问题,请参考以下文章