如何在 WPF DataGrid 中动态生成列?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1983033/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do I dynamically generate columns in a WPF DataGrid?
提问by dkackman
I am attempting to display the results of a query in a WPF datagrid. The ItemsSource type I am binding to is IEnumerable<dynamic>
. As the fields returned are not determined until runtime I don't know the type of the data until the query is evaluated. Each "row" is returned as an ExpandoObject
with dynamic properties representing the fields.
我正在尝试在 WPF 数据网格中显示查询结果。我绑定到的 ItemsSource 类型是IEnumerable<dynamic>
. 由于返回的字段直到运行时才确定,因此在评估查询之前我不知道数据的类型。每个“行”都作为ExpandoObject
具有代表字段的动态属性返回。
It was my hope that AutoGenerateColumns
(like below) would be able to generate columns from an ExpandoObject
like it does with a static type but it does not appear to.
我希望AutoGenerateColumns
(如下所示)能够ExpandoObject
像使用静态类型一样生成列,但它似乎没有。
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Results}"/>
Is there anyway to do this declaratively or do I have to hook in imperatively with some C#?
无论如何要以声明方式执行此操作,还是必须使用某些 C# 强制挂钩?
EDIT
编辑
Ok this will get me the correct columns:
好的,这将为我提供正确的列:
// 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 });
So now just need to figure out how to bind the columns to the IDictionary values.
所以现在只需要弄清楚如何将列绑定到 IDictionary 值。
采纳答案by dkackman
Ultimately I needed to do two things:
最终我需要做两件事:
- Generate the columns manually from the list of properties returned by the query
- Set up a DataBinding object
- 从查询返回的属性列表中手动生成列
- 设置数据绑定对象
After that the built-in data binding kicked in and worked fine and didn't seem to have any issue getting the property values out of the ExpandoObject
.
之后,内置数据绑定启动并运行良好,似乎没有任何问题从ExpandoObject
.
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Results}" />
and
和
// 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);
}
回答by Egor
The problem here is that the clr will create columns for the ExpandoObject itself - but there is no guarantee that a group of ExpandoObjects share the same properties between each other, no rule for the engine to know which columns need to be created.
这里的问题是 clr 将为 ExpandoObject 本身创建列 - 但不能保证一组 ExpandoObject 在彼此之间共享相同的属性,引擎没有规则知道需要创建哪些列。
Perhaps something like Linq anonymous types would work better for you. I don't know what kind of a datagrid you are using, but binding should should be identical for all of them. Here is a simple example for the telerik datagrid.
link to telerik forums
也许像 Linq 匿名类型这样的东西更适合你。我不知道您使用的是哪种数据网格,但所有这些数据网格的绑定都应该相同。这是 Telerik 数据网格的一个简单示例。
链接到 Telerik 论坛
This isn't actually truly dynamic, the types need to be known at compile time - but this is an easy way of setting something like this at runtime.
这实际上并不是真正的动态,需要在编译时知道类型 - 但这是在运行时设置类似内容的一种简单方法。
If you truly have no idea what kind of fields you will be displaying the problem gets a little more hairy. Possible solutions are:
如果您真的不知道将显示什么样的字段,问题就会变得更加棘手。可能的解决方案是:
- Creating a type mapping at runtime by using Reflection.Emit, I think it's possible to create a generic value converter that would accept your query results, create a new type (and maintain a cached list), and return a list of objects. Creating a new dynamic type would follow the same algorithm as you already use for creating the ExpandoObjects
MSDN on Reflection.Emit
An old but useful article on codeproject - Using Dynamic Linq - this is probably the simpler faster way to do it.
Using Dynamic Linq
Getting around anonymous type headaches with dynamic linq
- 通过使用 Reflection.Emit 在运行时创建类型映射,我认为可以创建一个通用值转换器来接受您的查询结果,创建一个新类型(并维护一个缓存列表),并返回一个对象列表。创建新的动态类型将遵循与您
在 Reflection.Emit 上创建 ExpandoObjects MSDN 时已经使用的算法相同的算法。
关于 codeproject 的旧但有用的文章 - 使用动态 Linq - 这可能是更简单、更快的方法。
使用
动态 linq 使用动态 linq 解决匿名类型的问题
With dynamic linq you can create anonymous types using a string at runtime - which you can assemble from the results of your query. Example usage from the second link:
使用动态 linq,您可以在运行时使用字符串创建匿名类型 - 您可以从查询结果中组合。第二个链接中的示例用法:
var orders = db.Orders.Where("OrderDate > @0", DateTime.Now.AddDays(-30)).Select("new(OrderID, OrderDate)");
In any case, the basic idea is to somehow set the itemgrid to a collection of objects whose sharedpublic properties can be found by reflection.
在任何情况下,基本思想都是以某种方式将 itemgrid 设置为一组对象,这些对象的共享公共属性可以通过反射找到。
回答by kenwarner
my answer from Dynamic column binding in Xaml
我的答案来自Xaml 中的动态列绑定
I've used an approach that follows the pattern of this pseudocode
我使用了一种遵循此伪代码模式的方法
columns = New DynamicTypeColumnList()
columns.Add(New DynamicTypeColumn("Name", GetType(String)))
dynamicType = DynamicTypeHelper.GetDynamicType(columns)
DynamicTypeHelper.GetDynamicType() generates a type with simple properties. See this postfor the details on how to generate such a type
DynamicTypeHelper.GetDynamicType() 生成具有简单属性的类型。有关如何生成此类类型的详细信息,请参阅此帖子
Then to actually use the type, do something like this
然后要实际使用该类型,请执行以下操作
Dim rows as List(Of DynamicItem)
Dim row As DynamicItem = CType(Activator.CreateInstance(dynamicType), DynamicItem)
row("Name") = "Foo"
rows.Add(row)
dataGrid.DataContext = rows
回答by Gábor
Although there is an accepted answer by the OP, it uses AutoGenerateColumns="False"
which is not exactly what the original question asked for. Fortunately, it can be solved with auto-generated columns as well. The key to the solution is the DynamicObject
that can have both static and dynamic properties:
尽管 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;
}
For the ICustomTypeDescriptor
implementation, you can mostly use the static functions of TypeDescriptor
in a trivial manner. GetProperties()
is the one that requires real implementation: reading the existing properties and adding your dynamic ones.
对于ICustomTypeDescriptor
实现,您主要可以TypeDescriptor
以简单的方式使用 的静态函数。GetProperties()
是需要真正实现的:读取现有属性并添加动态属性。
As PropertyDescriptor
is abstract, you have to inherit it:
作为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) {
...
}