2009-12-30 12 views
26

Estoy intentando mostrar los resultados de una consulta en una cuadrícula de datos 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 ExpandoObject con propiedades dinámicas que representan los campos.¿Cómo genero dinámicamente columnas en un WGD DataGrid?

Era mi esperanza que AutoGenerateColumns (como abajo) sería capaz de generar columnas desde un ExpandoObject como lo hace con un tipo estático pero parece que no.

<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Results}"/> 

¿Hay alguna forma de hacerlo declarativamente o tengo que enganchar imperativamente con C#?

EDITAR

Muy bien, este me llevará 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 sólo tiene que encontrar la manera de unir las columnas a los valores IDictionary.

Respuesta

24

En última instancia que tenía que hacer dos cosas:

  1. Generar la columnas manualmente de la lista de propiedades devueltas por la consulta
  2. Configurar un objeto DataBinding

Después de eso, el enlace de datos incorporado se ejecutó y funcionó bien y no parecía tener ningún problema para obtener los valores de propiedad del 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); 
} 
+1

Esto funciona bien, pero ¿cuándo ejecutas este código? El ItemsSource aún no está configurado cuando maneja esto en DataContextChanged – Wouter

+0

En mi instancia ItemSource está vinculada a una propiedad de ViewModel llamada Results. Tengo un controlador INotifyPrpertyChanged en la vista que reacciona al cambio de esa propiedad. – dkackman

+0

Ese fue mi enfoque, pero me encontré con un problema. ¿Qué pasa con Validación de Fila? ¿Tenía que manejar la validación de filas en ExpandoObjects? – Ninglin

5

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 .

Tal vez algo como los tipos anónimos de Linq funcionarían mejor para usted. 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.
link to telerik forums

Esto no es en realidad verdaderamente dinámica, los tipos tienen que ser conocido en tiempo de compilación - pero esto es una manera fácil de crear 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:

Con LINQ dinámica puede crear tipos anónimos utilizando una cadena en tiempo de ejecución - que se puede ensamblar a partir de los resultados de la consulta. Ejemplo de uso del 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 fijar de alguna manera el itemgrid a una colección de objetos cuya compartida propiedades públicas se pueden encontrar por la reflexión.

+0

Los datos proceden de las etiquetas dentro de los archivos mp3, por lo que el conjunto de hecho no es consistente. Y de hecho no hay conocimiento de compilación de lo que serán. Puedo evitar el problema de la coherencia de la propiedad, es lamentable que ExpandoObject sea opaco para la reflexión (aunque puedo ver que es un problema difícil de resolver). – dkackman

+0

En ese caso, el linq dinámico puede ayudar, pero es posible que necesite un enfoque de dos pasos. Analice los datos una vez para ver qué etiquetas se encuentran, y luego otra vez para completar la lista de objetos nuevos. Supongo que el problema es que si un archivo mp3 tiene una propiedad definida, después de asignar los valores a los objetos (dinámicos o no), todos deben tener esa propiedad. – Egor

4

mi respuesta de Dynamic column binding in 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. Ver this post para los detalles sobre cómo generar un tipo tal

Luego de utilizar realmente el tipo, hacer 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 
+1

Enfoque interesante. Probablemente tendré que hacer algo similar pero me gustaría evitar las piezas Emit. Usar ambos tipos Expando y Emitted parece redundante. Gracias por el enlace; me ha dado algunas ideas. – dkackman

1

Aunque hay una respuesta aceptada por el OP, se utiliza AutoGenerateColumns="False" que no es exactamente lo que la pregunta original pidió. Afortunadamente, también se puede resolver con columnas autogeneradas. La clave para la solución es la DynamicObject que pueden 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 ejecución ICustomTypeDescriptor, se puede utilizar la mayoría de las funciones estáticas de TypeDescriptor de una manera trivial. GetProperties() es el que requiere una implementación real: leer las propiedades existentes y agregar las dinámicas.

Como PropertyDescriptor es abstracto, tiene que heredará,

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) { 
    ... 
    } 
+0

Esto no parece funcionar para mí al vincular 'ItemsSource' a un' ObservableCollection ' – georgiosd

+0

Esta es la pieza faltante: http: //www.reimers .dk/jacob-reimers-blog/auto-generate-datagrid-columns-from-dynamicobjects – georgiosd

+0

Ese enlace está ahora muerto; ¿Alguien podría publicar la respuesta completa aquí? – Sphynx

Cuestiones relacionadas