wpf - obtener - ¿Cómo genero dinámicamente columnas en un WGD DataGrid?
obtener valor de celda datagrid wpf (4)
Estoy intentando mostrar los resultados de una consulta en una cuadrícula de datos de WPF. El tipo de ItemsSource al que me estoy vinculando es IEnumerable<dynamic>
. Como los campos devueltos no se determinan hasta el tiempo de ejecución, no sé el tipo de datos hasta que se evalúa la consulta. Cada "fila" se devuelve como un ExpandoObject
con propiedades dinámicas que representan los campos.
Tenía la esperanza de que AutoGenerateColumns
(como a continuación) fuera capaz de generar columnas desde un ExpandoObject
como lo hace con un tipo estático, pero no parece que lo haga.
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Results}"/>
¿Hay alguna forma de hacerlo declarativamente o tengo que engancharme imperativamente con C #?
EDITAR
Ok, esto me dará las columnas correctas:
// 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 });
Así que ahora solo necesitamos descubrir cómo vincular las columnas a los valores IDictionary.
Aunque hay una respuesta aceptada por el OP, usa AutoGenerateColumns="False"
que no es exactamente lo que la pregunta original solicitó. Afortunadamente, también se puede resolver con columnas autogeneradas. La clave de la solución es el DynamicObject
que puede tener propiedades estáticas y dinámicas:
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;
}
Para la implementación de ICustomTypeDescriptor
, puede usar principalmente las funciones estáticas de TypeDescriptor
de manera trivial. GetProperties()
es el que requiere una implementación real: leer las propiedades existentes y agregar las dinámicas.
Como PropertyDescriptor
es abstracto, debes heredarlo:
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) {
...
}
El problema aquí es que clr creará columnas para el propio ExpandoObject, pero no hay garantía de que un grupo de ExpandoObjects comparta las mismas propiedades entre sí, ninguna regla para que el motor sepa qué columnas deben crearse.
Quizás algo como los tipos anónimos de Linq funcionarían mejor para ti. No sé qué tipo de cuadrícula de datos está utilizando, pero el enlace debería ser idéntico para todos ellos. Aquí hay un ejemplo simple para telerik datagrid.
enlace a los foros de telerik
Esto no es realmente realmente dinámico, los tipos necesitan ser conocidos en tiempo de compilación, pero esta es una manera fácil de establecer algo como esto en tiempo de ejecución.
Si realmente no tienes idea de qué tipo de campos mostrarás, el problema se pone un poco más peludo. Las posibles soluciones son:
- Crear un mapeo de tipo en tiempo de ejecución utilizando Reflection.Emit, creo que es posible crear un convertidor de valor genérico que acepte los resultados de su consulta, cree un nuevo tipo (y mantenga una lista en caché) y devuelva una lista de objetos. Crear un nuevo tipo dinámico seguiría el mismo algoritmo que ya usa para crear los ExpandoObjects
MSDN en Reflection.Emit
Un artículo viejo pero útil en codeproject - Usar Dynamic Linq: esta es probablemente la manera más simple y rápida de hacerlo.
Usando Dynamic Linq
Cómo evitar dolores de cabeza tipo anónimos con linq dinámico
Con dynamic linq puedes crear tipos anónimos utilizando una cadena en tiempo de ejecución, que puedes ensamblar a partir de los resultados de tu consulta. Ejemplo de uso desde el segundo enlace:
var orders = db.Orders.Where("OrderDate > @0", DateTime.Now.AddDays(-30)).Select("new(OrderID, OrderDate)");
En cualquier caso, la idea básica es establecer de algún modo la cuadrícula de elementos en una colección de objetos cuyas propiedades públicas compartidas se puedan encontrar por reflexión.
En definitiva, necesitaba hacer dos cosas:
- Genere las columnas manualmente desde la lista de propiedades devueltas por la consulta
- Configurar un objeto DataBinding
Después de eso, el enlace de datos incorporado se ExpandoObject
y funcionó bien y no parecía tener ningún problema para obtener los valores de propiedad de ExpandoObject
.
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Results}" />
y
// 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);
}
mi respuesta del enlace de columna dinámico en Xaml
He utilizado un enfoque que sigue el patrón de este pseudocódigo
columns = New DynamicTypeColumnList()
columns.Add(New DynamicTypeColumn("Name", GetType(String)))
dynamicType = DynamicTypeHelper.GetDynamicType(columns)
DynamicTypeHelper.GetDynamicType () genera un tipo con propiedades simples. Consulte esta publicación para obtener detalles sobre cómo generar dicho tipo
Luego, para usar realmente el tipo, haz algo como esto
Dim rows as List(Of DynamicItem)
Dim row As DynamicItem = CType(Activator.CreateInstance(dynamicType), DynamicItem)
row("Name") = "Foo"
rows.Add(row)
dataGrid.DataContext = rows