views:

720

answers:

4

Rewritten:

I could use some input, suggestions, and samples from some other minds on building a databindable collection.

The collection needs to provide a databindable, editable tree of items, but with a small twist: the items need to be one of two types, with each type providing slightly different characteristics. The two types of items are Folder and TreeItem. A Folder contains it's own list of items (again, either of Folder or TreeItem type) and a TreeItem does not contain a list.

My current approach is fairly close, but feels crufty. Essentially I have an abstract base class, TreeItemBase, which (in a round-about way) inherits from BindableList. Then I have two concrete derived types, Folder and TreeItem which both inherit from the abstract base class. The obvious flaw is that the TreeItem, which can't contain childitems, still inherits from BindingList; so it's up to some ugly hackery to pretend that it's not a collection.

Is BindingList<> a poor choice to base this collection on? Some of the DataBinding interfaces sound like they offer a higher degree of control over the databinding, but I haven't found one that's quite right. My ideal though is to provide a custom implementation that lets me control how databinding walks the collection and can inspect the concrete type of each element to determine if it contains a collection, or if it's a terminus in the tree.

Here's a quick cut of the XML to help visualize what I'm trying to represent; it's easy for me to cleanly code the structure and rules in XSD--but I'm just having a hard time translating it to .NET and supporting databinding.

<Items>
  <TreeItem xsi:type="Folder" name="Root">
    <TreeItem xsi:type="Folder" name="Sub1">
      <TreeItem xsi:type="TreeItem" name="Humm"/>
    </TreeItem>
    <TreeItem xsi:type="TreeItem" name="Bleh"/>
    <TreeItem xsi:type="Folder" name="Sub2">
      <TreeItem xsi:type="TreeItem" name="Boo!"/>
    </TreeItem>
  </TreeItem>
</Items>

Update: I've been working more on my approach and have come close to what I'd like to do using an interface rather than a base-class for the items, but have hit a snag. The issue I've run into is covered on a seperate question.

Ideally I'd like to use the abstract base-class approach so that the XML that's generated considers Folder and TreeItem to be complexTypes (without manual control of the serialization), but it's a negligible requirement.

+3  A: 

Maybe my depth of knowledge isn't big enough for this question, but couldn't you do this:

Have an interface, and 2 classes that implement the interface.

interface ITreeItem
{
    IEnumerable<ITreeItem> GetChildren();
}

class MyFolder : ITreeItem
{
    public IEnumerable<ITreeItem> GetChildren()
    {
        // TODO: Return the list of children
    }
}

class MyITreeItem : ITreeItem
{
    public IEnumerable<ITreeItem> GetChildren()
    {
        // TODO: Return the list of children
    }
}

Then if your goal is to databind the collection to some list, you should be able to do so with the IEnumerable collections. In each call to databind the collections, you should be able to check to see which type the item is:

foreach (var node in root.GetChildren())
{
    if (node is MyFolder)
    {
        var folder = (MyFolder)node;

        // Bind fields from the folder object
    }
    else if(node is MyTreeItem)
    {
        var folder = (MyTreeItem)node;

        // Bind fields from the tree item object
    }
}

I did something similar (I think) to this when I had a list nested inside another list. To display the data, I setup Nested ListView controls.

Sorry if this isn't what you're looking for, but hope it helps!

SkippyFire
I appreciate the input, I've been playing with your approach and quickly realized I forgot to specify that the list needs to be editable. Sorry for the lack of clarity
STW
+2  A: 

SkippyFire's solution seems more elegant than mine, but I figured that I will show you how I solved the problem. The following solution shows what I did to build a collection that can be bound to a tree view, and you can determine which items have been selected. It does not implement any Bindable Lists or anything though. However, it is not clear from your post whether this is what you want.

This is an example of my XML file:

 <controls name="Parent" attribute="element">
  <control name="Files" attribute="element">
    <control name="Cashflow" attribute="element">
      <control name="Upload" attribute="leaf"/>
      <control name="Download" attribute="leaf"/>
    </control>
  </control>
  <control name="Quotes" attribute="element">
     <control name="Client Quotes" attribute="leaf"/>
  </control>
</controls>

Then I have a class that represents each item. It contains a Name, a List of child nodes (1 level down), a reference to its parent, and a string that logs the attribute of the element.

 public class Items
    {
        public string Name { get; set; }
        public List<Items> SubCategories { get; set; }
        public string IsLeaf { get; set; }
        public string Parent { get; set; }
    }

From there, I populate a list of Items as follows:

List<Items> categories = new List<Items>();

XDocument categoriesXML = XDocument.Load("TreeviewControls.xml");

categories = this.GetCategories(categoriesXML.Element("controls"));

This calls the GetCategories() method

private List<Items> GetCategories(XElement element)
        {

            return (from category in element.Elements("control")
                    select new Items()
                    {
                        Parent = element.Attribute("name").Value,
                        Name = category.Attribute("name").Value,
                        SubCategories = this.GetCategories(category),
                        IsLeaf = category.Attribute("attribute").Value

                    }).ToList();
        }

After the categories variable has been populated, I just assign the list as the treeview's ItemSource.

controltree.ItemsSource = categories;

And from there, if the choice changes in the tree, I check if the choice is a leaf node, and if so, I raise an event.

private void Selection_Changed(object sender, RoutedEventArgs e)
        {
            Items x = controltree.SelectedItem as Items;

            if (x.IsLeaf.Equals("leaf"))
                _parent.RaiseChange(x.Parent+","+x.Name);
        }

This solution works for any depth in the tree as well.

Johannes
+1  A: 

I've used: treeviewadv from source forge. It have a very nice MVC way of dealing with tree view type modeling. It is a windows forms control that binds a model to a treeview style control with support for columns. They also provide some nice sample code.

Joe Caffeine
A: 

ObservableCollection is perhaps the best bet for you, coupled with two DataTemplates.

public class TreeItem : INotifyPropertyChanged
{
    public string Name { get; set; }
    // ...
}

public class Folder : TreeItem
{
    public ObservableCollection<TreeItem> Items { get; private set; }

    public Folder()
    {
        this.Items = new ObservableCollection<TreeItem>();
    }
    // ...
}

And your DataTemplate's (including the Secret Sauce HierarchicalDataTemplate):

<HierarchicalDataTemplate DataType="{x:Type local:Folder}"
                          ItemsSource="{Binding Path=Items}">
    <TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:TreeItem}">
    <TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>

Putting it all together (code behind):

public class FolderList : ObservableCollection<TreeItem>
{
    public FolderList()
    {
        this.Add(new TreeItem { Name = "Hello" });
        this.Add(new TreeItem { Name = "World" });

        var folder = new Folder { Name = "Hello World" };
        folder.Items.Add(new TreeItem { Name = "Testing" });
        folder.Items.Add(new TreeItem { Name = "1" });
        folder.Items.Add(new TreeItem { Name = "2" });
        folder.Items.Add(new TreeItem { Name = "3" });
        this.Add(folder);
    }
}

XAML:

<Grid>
    <Grid.Resources>
        <local:FolderList x:Key="MyItems" />
        <HierarchicalDataTemplate DataType="{x:Type local:Folder}"
                                  ItemsSource="{Binding Path=Items}">
            <TextBlock Text="{Binding Path=Name}"/>
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type local:TreeItem}">
            <TextBlock Text="{Binding Path=Name}"/>
        </DataTemplate>
    </Grid.Resources>
    <TreeView>
      <TreeViewItem ItemsSource="{Binding Source={StaticResource MyItems}}"
                    Header="Root" />
    </TreeView>
</Grid>
sixlettervariables