Skip to content

Nested dynamic objects

New Discussion
Jared Lessl
Jared Lessl asked on May 16, 2019 7:11 PM

Apologies in advance, the work VM I do the coding on doesn’t have internet access nor let me copy-paste code out of it, so it’s gotta be all descriptions.

I’m trying to use UltraGrid to display the contents of a list of nested ExpandoObjects.  The list (ExpandoList) is derived from BindingList<object> that implements ITypedList; the GetItemProperties() method checks if the first item is an ExpandoObject, in which case it builds a list of PropertyDescriptors constructed from the contents of that first item.

This all works great.  But when I test with one property that is another ExpandoList of dynamic objects, the grid creates a new child band for it, but reuses the properties from the parent ExpandoObject to build the child band’s columns, not the child’s properties.  I’ve stepped through it, it’s definitely requesting and getting the property descriptors for the child.  And if I break in the InitializeLayout event, it actually shows two child bands: one with the parent properties (wrong) which is being used, and one with the child properties (right) which is not.

If I test the same data with strongly typed classes instead, it loads the child band columns correctly.  So the only thing I can think of is that since both parent and child are technically the same type (ExpandoObject), the grid is caching the properties it got for that type from the parent and reusing it for the child.  But then why is it bothering to request the child properties at all?

Is there any way to make this work?  Or is there a better way to get the grid to display nested, unknown-at-design-time data?

Sign In to post a reply

Replies

  • 0
    Jared Lessl
    Jared Lessl answered on May 15, 2019 11:54 AM

    Used a custom Dictionary<string, object> storage class that implements ICustomTypeDescriptor, that seemed to work.

  • 0
    Mike Saltzman
    Mike Saltzman answered on May 15, 2019 1:18 PM

    Hi Jared, 

    I'm having a little trouble following exactly what you are doing. There are a lot of ambiguities here in your description. 

    But it sounds like you are implementing ITypedList and implementing GetProperties to establish the structure of your data. In theory, this should work, but there are a lot of caveats to be aware of. For one thing, the grid can't display data that isn't constistent. In other words, the columns of every row in the band have to be the same. You can't have two rows in the same band that have different structures. This includes child rows under different parents. It's not clear from your description if that's what you are doing, but you seem to be pushing the envelope with your use of ITypeList here. 

    It's also possible that something somewhere is caching the property descriptors, and it seems to be that you are essentially returning different PropertyDescriptors from GetProperties at different times for the same type. That seems like a pretty odd thing to do, and I am pretty sure that the WinGrid isn't expecting that, nor is the BindingManager. So either of these objects could be assuming that GetProperies for a specific type will always return the same properties and thus is might be caching the PropertyDescriptors. 

    As I said, it's really hard for me to understand exactly what you are doing here from just this basic description. There are a lot of details missing and it's all pretty vague. I understand that you can't send us your real code for whatever reason, but perhaps you could try to duplicate what you are doing and the resulting problems in a small sample project. If I could run and test a sample I could tell you more about exactly what's happening and potentially how to get it to work correctly. 

    Another option you might consider is to simply insert an intermediary between the grid and the data source. You could use the UltraDataSource in OnDemand mode as your grid's DataSource. That would allow you to define the parent and child band structure yourself and then you just have to handle the UltraDataSource events to get and set data on the "real" data source. 

    • 0
      Jared Lessl
      Jared Lessl answered on May 15, 2019 3:02 PM

      I’m trying to take an inputted list of JSON data structures and display the contents in the grid.  Which means I will know absolutely nothing about the structure beforehand.

      > the columns of every row in the band have to be the same

      So if I have data structured like this:

      • A1
        • B1
          • C1
        • B2
          • C2
          • C3
      • A2
        • B3
          • C4

      You’re saying that while the A’s, B’s, and C’s can differ from each other, they would need to be consistent within themselves?  Shouldn’t be a problem.  In theory, there’s no enforcing that sort of compliance, but in practice they’ll pretty much be the same.  This is for a debugging tool anyway.

      > it seems to be that you are essentially returning different PropertyDescriptors from GetProperties at different times for the same type

      Correct.  Which is unavoidable, because it’s just a generic dictionary type.


      But anyway, I discovered that returning the PropertyDescriptors in the dictionary object itself (via ICustomTypeDescriptor) rather than from the containing list worked.  Just required a custom JsonConverter in the mix to deserialize all objects to PropertyBags.

       

      Here’s the gist of it.

      class PropertyBag : Dictionary<string, object>, ICustomTypeDescriptor
      {
          public PropertyDescriptorCollection GetProperties()
          {
            return new PropertyDescriptorCollection(this.Select(kvp => new CustomPropertyDescriptor(kvp.Key, kvp.Value?.GetType())).ToArray());
          }
      }
      
      class CustomPropertyDescriptor : PropertyDescriptor
      {
          private string _Name;
          private Type _Type;
      
          public CustomPropertyDescriptor(string name, Type type): base(name, new Attribute[0])
          {
              _Name = name;
              _Type = type;
          }
      
          public override object GetValue(object component)
          {
              var bag = (PropertyBag)component;
              if (bag != null) return bag[_Name];
              return null;
          }
      }

       

       

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 15, 2019 5:22 PM

        Yes, just to clarify, A1 and A2 in this example have to have the same properties – the same data structure. And the same is true for B1, B2, and B3. And, of course, for the Cs, as well. You can't have two rows in the same band that have different columns. 

        I'm not sure I'm following what you are doing with the dictionary here. But generally speaking, a Dictionary<> is not a good data source for the WinGrid, because it doesn't support a bunch of things the grid needs for certain functionality. You will not be able to add or delete rows from the grid, for example. Maybe you don't need that. 

        In theory, binding the grid using a BindingList<T> that implements ITypedList and returns different property descriptors for each level of the data should work, assuming that the PropertyDescriptors are always consistent for the band. But like I said, it's also possible that something somewhere along the way, like the BindingManager is making the assumption that the same type will always return the same PropertyDescriptors and is caching them for performance instead of asking for them every time – and that if the same type returns a different set of PropertyDescriptors, things might not work right.

        So it seems like what you are essentially doing here is what I described when I mentioned the UltraDataSource. You are using the BindingList as a sort've intermediary between your JSON objects and the grid. If it were me, I'd use UltraDataSource instead, rather than creating your own custom BindingLists and objects. But I suppose if it's already working for you, there's no point starting over. 

      • 0
        Jared Lessl
        Jared Lessl answered on May 15, 2019 5:51 PM

        > You will not be able to add or delete rows from the grid

        This is read-only.  But I still could, since the grid is being passed a list of dictionaries masquerading as strongly-typed objects.  Hence the PropertyDescriptor.GetValue() logic (there's a matching SetValue() method that I didn't type out here).  It would have no trouble adding a new PropertyBag to the list and setting values using the properties obtained from other instances of the same band-type.

        The customized BindingList didn't work for the child objects.  I wound up being able to use a vanilla BindingList<object>.  The logic is all in the object itself.  Still the same object type for all bands, but I guess the grid doesn't cache/reuse property descriptions when the ICustomTypeDescriptor is in play.

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 16, 2019 1:22 PM

        Okay, well, I will have to take your word for it, since I'm still unclear on exactly what you are binding to or what you are using the Dictionary for. If you bind the grid to a Dictionary, then the user will not be able to add rows to the grid, since Dictionary doesn't implement IBindingList or any interface that allows the grid or the BindingManager to add rows. 

        If you want to keep pursuing the issue, then we'd need a sample demonstrating exactly what your data source is doing so we can see the problem and debug it to figure out why one of the grid bands is showing the wrong columns. It's possible there is a bug somewhere in the grid where it's making a bad assumption there, but without the ability to reproduce the problem, there's really no way to be sure. 

        It seems like you have a solution, though, so it's probably not worth spending any more time on. But I would advise you to test any features of the grid that you need like adding, updating, or deleting, just to make sure your current solution can do everything you need your application to do. 

      • 0
        Jared Lessl
        Jared Lessl answered on May 16, 2019 1:56 PM

        No, I'm binding the grid to a BindingList of dictionaries.  That's why I can add/delete rows, and the SetValue method in the property descriptor lets me change the dictionary entries as if they were real properties.  In fact, one of the things I had to do in the custom JsonConverter was to rig it so that any non-list child objects were instead converted to a single-item list containing that object, otherwise the corresponding child band really would be bound to a dictionary.

        Nah, probably won't bother.  I've long since trashed that version of it anyhow, would have to rewrite it.  Thanks anyway, though!

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 16, 2019 2:03 PM

        Okay, that makes things clearer for me. 

        My understanding is that the BindingManager checks for ITypedList first. So in the original case, you were implementing ITypedList and I would think that should work. You would have to explicitly implement ITypedList, of course, since you were using BindingList<object>. But as long as you were implementing it and returning the same structure consistently for the band, that should work. 

        If they fail to get the structure from IBindingList, then I think the BindingManager's next step is to use the first row of data and get the structure from that. So that must be what's happening here. 

        Seems like either there must be something wrong in your imlpementation of IBindingList or else the BindingManager is doing something inconsistent in where it gets the structure from. 

      • 0
        Jared Lessl
        Jared Lessl answered on May 16, 2019 2:09 PM

        Yup.  And indeed, it did work, for the top-level band.  My original problem was that it would then clone that band for any child lists, instead of using the appropriate property descriptors for it.

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 16, 2019 2:13 PM

        So it was the child bands that were wrong? The root band was correct and the child bands showed the same columns as the root band… which was incorrect?

      • 0
        Jared Lessl
        Jared Lessl answered on May 16, 2019 2:15 PM

        Correct.

        This all works great.  But when I test with one property that is another ExpandoList of dynamic objects, the grid creates a new child band for it, but reuses the properties from the parent ExpandoObject to build the child band's columns, not the child's properties

        Sorry, I should have posted a screenshot.

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 16, 2019 3:50 PM

        Okay, now that I have a clearer understanding of what you are doing, I tried to duplicate this issue. And I am, in fact, seeing the same problem. 

        But I think this must be an issue with the BindingManager and not the grid. I suspect it's doing some kind of caching. My basis for that assertion is that I tried using the same data source with the inbox DataGridView and I get the same results. It's slightly different, because the DataGridView doesn't show hierarchical data. But what I did was created two DataGridView controls – one bound to the root level and one bound to the child data. And the child grid shows the columns from the parent. 

        I even tried creating a separate type for the parent and child lists, and I still get the same results.

        I think there must be some other interface or something that we are supposed to implement here that allows the BindingManager to identify that the list is different. I would have thought that would be the ITypedList.ListName, but that doesn't seem to work. It looks like they are simply using the type of list (BindingList<object>) here. IBindingList doesn't seem to have any sort of Name property on it. 

        I am attaching my sample here just in case it comes up again in the future. But it's an interesting problem. 1030.WindowsFormsApp1.zip

      • 0
        Jared Lessl
        Jared Lessl answered on May 16, 2019 4:08 PM

        Yeah, I had tried returning something unique in the ListName too, no luck.

        And yet strangely, it's smart enough to requery the ICustomTypeDescriptor.GetProperties, for the same type.  Go figure.  I'll try mucking around with BindingManagerBase sometime, see if anything comes of it.


        Say, while I've got you here, I'm running into an issue with data extracted from these dictionary pseudo-properties.  The UltraGrid is rendering auto-generated datetime columns as date-only, with no time info.  How do I change the default column format for datetime values?  And is there any way to get it to examine the actual contents of the column first, even just the first row?  Because often enough it really will be a date-only column, and then the date-only formatting would be preferable to "… 12:00:00 AM".

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 16, 2019 6:13 PM

        I figured it out. We were doing it wrong. When they ask for the child list properties, they don't call into GetItemProperties on the child list. They call into GetItemProperty on the root-level and pass in a PropertyDescriptor to the listAccessors parameter to indicate they want the child data structure. 

        So in my sample, it works correctly if I do this: 

                PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
                {
                    switch (this.Level)
                    {
                        case 0:
                            if (null == listAccessors ||
                                listAccessors.Length == 0)
                                return this.RootList;
        
                            var childProp = listAccessors[0];
                            if (childProp == this.rootList[3])
                                return this.ChildList;
        
                            break;
        
                        case 1:                    
                           return this.ChildList;
                    }
        
                    Debug.Fail("Not supported.");
                    return null;
        
                }

      • 0
        Jared Lessl
        Jared Lessl answered on May 16, 2019 7:11 PM

        Outstanding!  Thank you very much.

      • 0
        Jared Lessl
        Jared Lessl answered on May 16, 2019 4:48 PM

        Think I may have it. 

        In the InitializeLayout event, iterate through the bands and their columns, check the DataType property.  If it's DateTime, then set the Format property accordingly.

        To get the contents, call the band's GetRowEnumerator().OfType<UltraGridRow>(), and examine the cell values, if there are any, for the column in question.  If TotalSeconds is zero, it's probably a date-only column.  If the Milliseconds is zero, it's probably a seconds-precision time. 

        Any better method I'm not seeing?

      • 0
        Mike Saltzman
        Mike Saltzman answered on May 16, 2019 6:15 PM

        No, there's no way to change the default. You have to set the Format on each column. 🙂 

  • You must be logged in to reply to this topic.
Discussion created by
Favorites
Replies
Created On
Last Post
Discussion created by
Jared Lessl
Favorites
0
Replies
17
Created On
May 16, 2019
Last Post
6 years, 10 months ago

Suggested Discussions

Tags

No tags

Created by

Created on

May 16, 2019 7:11 PM

Last activity on

Feb 23, 2026 8:26 AM