views:

661

answers:

2

Problem: The data I'm trying to display is essentially a collection of collections of some object. So the rows can be any number, normal for a datagrid, and the columns are also any number. That's not so normal for a datagrid. Usually you have a set number of columns and your rows vary. The datagrid cell will be either a string or a value changeable via a combobox.

Attempted Solution: I tried adding columns to the datagrid dynamically, and while this worked just fine (adding them in codebehind) the real issue I hit was how to bind to the underlaying objects. The data is being built dynamically and I tried a couple of formats. I tried a collection of Arrays, and also an ObservableCollection of ObservableCollections. I could bind to the objects but as Bindings in Silverlight have to bind to properties I couldn't come up with a solution presenting the data this way.

My solution as a result has been to display the data in a more traditional manner, with a list and a datagrid. When you select an item in the list it repopulates the data in the datagrid to show the objects.

Question: is there a way to bind a datagrid cell to a collection of collections of objects?

I found this question (WPF) which looks similar and it didn't help any. I think it's the same issue. http://stackoverflow.com/questions/1633800/wpf-datagrid-datagridcomboxbox-itemssource-binding-to-a-collection-of-collection

+1  A: 

One possible solution is instead of using a simple collection as the inner object, create a class derived from a collection and implement ICustomTypeDescriptor on it. In the interface implementation, iterate over the elements of the collection and populate your property descriptor collection accordingly. Once you do that, you should be able to bind to those properties from XAML.

Example - a data object based on a dictionary, which you can bind against its key names (I compressed to a single line all trivial method implementations):

class DictionaryDataObject : Dictionary<string, object>, ICustomTypeDescriptor
{
    #region ICustomTypeDescriptor Members

    public AttributeCollection GetAttributes() { return AttributeCollection.Empty; }
    public string GetClassName() { return "DictionaryDataObject"; }
    public string GetComponentName() { return null; }
    public TypeConverter GetConverter() { return null; }
    public EventDescriptor GetDefaultEvent() { return null; }
    public PropertyDescriptor GetDefaultProperty() { return null; }
    public object GetEditor(Type editorBaseType) { return null; }
    public EventDescriptorCollection GetEvents(Attribute[] attributes) { return EventDescriptorCollection.Empty; }
    public EventDescriptorCollection GetEvents() { return EventDescriptorCollection.Empty; }
    public PropertyDescriptorCollection GetProperties() { return GetProperties(null); }
    public object GetPropertyOwner(PropertyDescriptor pd) { return this; }

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        var pds =
            this.Keys
            .Select(x => new DictionaryPropertyDescriptor(x))
            .ToArray();
        return new PropertyDescriptorCollection(pds);
    }

    #endregion
}

class DictionaryPropertyDescriptor : PropertyDescriptor
{
    public DictionaryPropertyDescriptor(string name) : base(name, null) { }
    public override bool CanResetValue(object component) { return false; }
    public override Type ComponentType { get { return null; } }
    public override bool IsReadOnly { get { return false; } }
    public override Type PropertyType { get { return typeof(object); } }
    public override void ResetValue(object component) { }
    public override bool ShouldSerializeValue(object component) { return false; }

    public override object GetValue(object component)
    {
        var dic = component as DictionaryDataObject;
        if (dic == null) return null;
        return dic[Name];
    }

    public override void SetValue(object component, object value)
    {
        var dic = component as DictionaryDataObject;
        if (dic == null) return;
        dic[Name] = value;
    }
}

Sample object setup from code behind:

DictionaryDataObject ddo = new DictionaryDataObject();

public Window4()
{
    ddo["propa"] = 1;
    ddo["propb"] = "foo";
    ddo["propc"] = "bar";
    ddo["propd"] = 4.5;
    InitializeComponent();
    DataContext = ddo;
}

XAML usage:

<Window.Resources>
    <DataTemplate x:Key="template">
        <WrapPanel>
            <TextBlock Text="{Binding propa}" Margin="5"/>
            <TextBlock Text="{Binding propb}" Margin="5"/>
            <TextBlock Text="{Binding propc}" Margin="5"/>
            <TextBlock Text="{Binding propd}" Margin="5"/>
        </WrapPanel>
    </DataTemplate>
</Window.Resources>
<Grid>
    <ContentControl Content="{Binding}" ContentTemplate="{StaticResource template}"/>
</Grid>

If you want to adapt this solution to a list instead of a dictionary, you must set each property name based on some property of the list element.

Aviad P.
Thanks, I'll try this out. Still seems a bit more complicated than I was hoping but if it works nice to know there's a work around. cheers
Stephen Price
A: 

I think I understand what you are trying to accomplish and I actually have a more elegant solution to your problem and it does not involve writing any custom classes. I wrote a blog post on this issue. The blog is geared towards the DataGrid from the Silverlight Toolkit, but you can easily modify it to use any grid.

The solution is here.

Let me know if this is what you were looking for.

I've had a read and your solution looks like a really elegant one for sure. I did not realise that you could bind to a collection (like you have done in the Template string). ie Text='{{Binding Periods[{0}].{1}}}'I thought that was limited to WPF and Silverlight couldn't do it. I'll investigate further. thanks!
Stephen Price