如何在 WPF DataGrid 中动态生成列?
-
22-09-2019 - |
题
我正在尝试在 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 值。
解决方案
最终我需要做两件事:
- 从查询返回的属性列表手动生成列
- 设置 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);
}
其他提示
这里的问题是 clr 将为 ExpandoObject 本身创建列 - 但不能保证一组 ExpandoObject 彼此共享相同的属性,引擎没有规则知道需要创建哪些列。
也许像 Linq 匿名类型这样的东西更适合你。我不知道您使用的是哪种数据网格,但所有数据网格的绑定应该相同。这是 Telerik 数据网格的一个简单示例。
Telerik 论坛链接
这实际上并不是真正的动态,需要在编译时知道类型 - 但这是在运行时设置此类内容的简单方法。
如果您确实不知道要显示什么样的字段,那么问题就会变得更加棘手。可能的解决方案是:
- 通过使用 Reflection.Emit 在运行时创建类型映射,我认为可以创建一个通用值转换器来接受查询结果、创建新类型(并维护缓存列表)并返回对象列表。创建新的动态类型将遵循与创建 ExpandoObjects 相同的算法
MSDN 关于 Reflection.Emit
关于 codeproject 的一篇古老但有用的文章 - 使用 Dynamic Linq - 这可能是更简单、更快的方法。
使用动态 Linq
使用动态 linq 解决匿名类型问题
通过动态 linq,您可以在运行时使用字符串创建匿名类型 - 您可以从查询结果中组装该类型。第二个链接的用法示例:
var orders = db.Orders.Where("OrderDate > @0", DateTime.Now.AddDays(-30)).Select("new(OrderID, OrderDate)");
无论如何,基本思想是以某种方式将 itemgrid 设置为对象的集合,这些对象的集合 共享 公共属性可以通过反射找到。
我的回答从动态柱在XAML绑定
我用下面这个伪代码
的图案的方法columns = New DynamicTypeColumnList()
columns.Add(New DynamicTypeColumn("Name", GetType(String)))
dynamicType = DynamicTypeHelper.GetDynamicType(columns)
DynamicTypeHelper.GetDynamicType()生成具有简单的属性类型。请参见这个帖子为关于如何产生这种类型的细节
然后实际使用类型,做这样的事
Dim rows as List(Of DynamicItem)
Dim row As DynamicItem = CType(Activator.CreateInstance(dynamicType), DynamicItem)
row("Name") = "Foo"
rows.Add(row)
dataGrid.DataContext = rows
虽然由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) {
...
}