icelava.net

Re-wiring the brain, re-defining the human workflow
Welcome to icelava.net Sign in | Help
in Search

Databinding hierarchical collection to DataGrid

Last post 01-19-2006, 11:30 by icelava. 2 replies.
Sort Posts: Previous Next
  •  01-16-2006, 12:30 1215

    Databinding hierarchical collection to DataGrid

    The project I am engaged in now has this requirement to present different templates of questions (for different situations) for service operators to ask parties on the opposite side of a phone call. Called "scripts", I guess because some questions split down different branches of children questions depending on the answer. So one can see a rather wide range of possible playacting; some templates have over a hundred questions. So depending on what a parent question has for its answer, the relevant children questions must appear.

    Any object-oriented programmer could recognise this as a collection with depth, and would probably think of using a TreeView to build this out visually. Well, the UI constraint here is I have to present it with Infragistics UltraGrid. In fact, the entire script display engine was already in place and working in the production environment long before I joined the project.

    Now herein lies the problem.

    Without going into much detail (for this really warrants another lengthy horror story) I will summarise that the original programmer tried to simulate this hierarchy mechanism by sheer DataTable and array "acrobatics". So amazing that he himself couldn't understand what he did. Needless to say, it was one convoluted solution that contributed regularly to the project's defect count. And despite its age and "maturity" at the point I came along, the problems did not stop. Nearing two months of tolerating it, I finally gave up trying to understand the code and experimented with a cleaner display model.

    This should not be a new thing. Yet, I was unable to find any practical information of anybody else implementing a deep hierarchy of custom objects that is bindable to the flat view of a DataGrid (or UltraGrid in this case). In particular, how to achieve some important, if not crucial, things:
    1. Have the UltraGrid "walk" the hierarchy tree and display each item in their correct order (children under parent).
    2. Display only a limited subset of the hierarchy nodes' properties as columns in the UltraGrid.
    3. the confound the matter even worse, questions can be of several input types - Textbox, radio buttons, combo dropdowns, checkbox, DateTime, etc. All in the same column. This is where both the beauty and ugliness of the UltraGrid comes into full view, and contributed to much of the complexity of the old engine.
    I however do not wish to bring issue #3 into this scope, so I can keep it cleaner by focusing on building a bindable hierarchy collection.

    With the benefit of time granted during the long new year weekend, I was able to gather enough segregated information and successfully came up with a new model in use in the production environment now. Unfortunately I will have to save those details for another day - too beat to continue now (anybody have a power tonic to recommend?).
  •  01-17-2006, 11:57 1216 in reply to 1215

    Re: Databinding object hierarchy collection to DataGrid

    What makes a collection bindable to the DataGrid anyway? How do the DataTable, DataView, and DataSet present themselves to a DataGrid or other databound controls? According to the DataGrid's remarks, anything that implements IList or IListSource interfaces.

    So what do I have here really? A container (the root) of containers to the infinite level (questions with their own children questions). For a moment IListSource seemed like it since I don't exactly want to bind the actual root container object but the hierarchical nodes within. But how would each node handle its own collection? How am I supposed to return a singular uniform IList for an unknown number in a tree structure?

    Upon further inspection and thought, the answer lies in the IList's own implementation of the ICollection interface, and its Item property, which gave me a hint to how the DataGrid may be querying a collection to obtain a total count and iteration for each "data item".

    The root container is the only class (let's call it HierarchyCollection, and holding a bunch of HierarchyNodes) that ever needs to implement IList. For the practical purposes of this scenario it only has to possess logic in the ICollection.Count and IList.Item indexer properties.

            public int Count
            {
                get
                {
                    int counter = 0;

                    if (this.topLevelNodes != null) // internal ArrayList of nodes.
                    {
                        foreach (HierarchyNode node in this.topLevelNodes)
                        {
                            counter += node.CountNodesInTree();
                        }
                    }

                    return counter;
                }
            } // Count


    And each HierarchyNode contributes to the count recursively.

            public int CountNodesInTree()
            {
                int counter = 1; // counting itself.

                if (this.childrenNodes != null)
                {
                    foreach (HierarchyNode child in this.children)
                        counter += child.CountNodesInTree();
                }

                return counter;
            } // CountNodesInTree()


    One can of course, use a variable to keep a count of the nodes as they are inserted into the collection, so that processing is not necessary each time the property is accessed. That is up to you.

    So knowing how many items there are in the hierarchy is the first step. But which node has what ordinal number? We will do a similar walk again. Here is where the IList.Item property comes into play.

            public object this[int index]
            {
                get
                {
                    if (this.topLevelNodes == null)
                        return null; // No collection.

                    int walkCount = 0;

                    foreach (HierarchyNode node in this.topLevelNodes)
                    {
                        HierarchyNode indexedNode = node.FindNodeByIndex(index, ref walkCount);
                        if (indexedNode != null)
                            return indexedQuestion;
                    }

                    return null;
                } // get
               
                set
                {
                    // My implementation doesn't require modification ;-)
                    // insert custom set logic here.
                }
            } // Item indexer


    Since this is a zero-based index, the beginning of the walk starts with 0 (supposing there is at least one node existing in the collection), and is passed to the first node's tree.

            public HierarchyNode FindNodeByIndex(int soughtIndex, ref int walkCount)
            {
                if (soughtIndex == walkCount)
                    return this;

                walkCount ++;

                if (this.children == null)
                    return null;

                foreach (HierarchyNode child in this.children)
                {
                    HierarchyNode indexedNode = child.FindNodeByIndex(soughtIndex, ref walkCount);
                    if (indexedNode != null)
                    {
                        return indexedNode;
                    }   
                }

                return null;
            } // FindNodeByIndex()


    By passing this counter as a reference, each child node can increment it so as long as it does not match with the index number sought after. When it finally does match, we have found our item and return it back up the tree.

    One can also try a tailored binary search algorithm for possible faster searching through a huge hierarchy. That is something I have not had time to test out.

    And there you have it, a hierarchical collection of objects that can be presented "flat" to the DataGrid.

    I have again run out of time and energy. *yawn* the next part will actually be pretty straight forward - having each Hierarchy object bound to the DataGrid showing the correct columns. Till then.
  •  01-19-2006, 11:30 1218 in reply to 1215

    Re: Databinding object hierarchy collection to DataGrid

    A DataGrid without any solid column definition will seek out the public properties of the data item it is binding in. That means, any possible public property i may expose for my custom object will get sniffed out and rendered into column data in the DataGrid, disregarding whether I truly want it there or not. How can I control this?

    One of the early things I was advised to look at is the ICustomTypeDescriptor interface. But without any real teachings on the nature of this interface (the documentation just doesn't cut it), I was far from accomplishing what I needed.

    Knowing that I had to start somewhere else lower, I gave KB313482 a patient read. This is one MSKB article I can heartily recommend as a base for anyone looking into serious data-binding in Windows Form. Armed with this new knowledge, I proceeded to seek more detailed information about ICustomTypeDescriptor, convinced that is the key to this puzzle. Once again here, it becomes obvious why article writers and book authors are undeniably more famous and respected than documentation writers.

    MSDN Magazine is really one resource any .NET developer should not do without. What I like about it is the coverage on obscure portions of the .NET Framework that few know about, or even know how to use. Last year's series of the .NET MATTERS section saw Stephen Toub touching on the exact matter I was concerned with
    His manipulation of the interface is actually way more comprehensive than I ever need for my purposes, but the "tricks" he performs are pretty neat. In fact, the sample's so comprehensive I was able to copy and paste the code and made the little adjustments.

            public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
            {
                 // static cache.
                if (cachedPropertyDescriptors != null)
                    return cachedPropertyDescriptors;

                PropertyDescriptor[] descriptorArray = new PropertyDescriptor[1];
                PropertyDescriptorCollection typeProperties = TypeDescriptor.GetProperties(this.GetType());

                // Grab only the properties we need to expose for binding.
                foreach (PropertyDescriptor descriptor in typeProperties)
                {
                    if (descriptor.Name == "nvchQuestion")
                    {
                        descriptorArray[0] = descriptor;
                    }
                }

                cachedPropertyDescriptors = new PropertyDescriptorCollection(descriptorArray);
                return cachedPropertyDescriptors;
            } // GetProperties(Attribute[])


    The other method implementations were just re-routed back to the static TypeDescriptor to deal with. With these changes in place, my custom node objects were now clothing themselves properly and only revealing the necessary portions of themselves when asked to.

    And there we have it, a hierarchical collection with custom exposed properties  bound to the DataGrid.
View as RSS news feed in XML
Powered by Community Server, by Telligent Systems