TreePanel

TreePanel


TreePanel inherits from TablePanel, which is also the abstract parent class of GridPanel. This means that trees can get many features that GridPanels do (for example, multiple columns). A TreePanel can also work with a subclass of Store—the TreeStore—which is especially useful for AJAX-based tree interactions. These useful architectural changes from previous versions of Ext.NET increase familiarity and code reuse, while making TreePanels quite rich.

TreePanel – overview

We will leave the Store aspect for a later part of this chapter. In this section we will have a look at the basic mechanics of putting a TreePanel together.

A TreePanel is made up of nodes. A node may have children, which are more nodes. If a node does not have any child nodes it is referred to as a leaf node. A TreePanel has a single root node (which is optional to display). Tree nodes can be loaded upfront when the page is loaded (or when the TreePanel is constructed), or nodes can be expanded and populated locally or via the result of an AJAX request (remote loading). You can store additional data on each node through the use of custom attributes.

To illustrate, we will convert a folder containing icons in many subfolders into a TreePanel. This screenshot shows two types of TreePanel. The left-hand side shows the default look, while the right-hand side hides the root node and uses custom icons:

To produce the left hand TreePanel we can create the nodes manually as follows:

<ext:TreePanel runat="server" Width="250" Height="250"
               Icon="Pictures" Title="Icons" AutoScroll="true">
  <Root>
    <ext:Node Text="icons" Expanded="true">
      <Children>
        <ext:Node Text="16x16" Expanded="true">
          <Children>
            <ext:Node Text="actions" Leaf="true" />
            <ext:Node Text="apps" Leaf="true" />
            <ext:Node Text="devices" Leaf="true" />
            <ext:Node Text="filesystems" Leaf="true" />
            <ext:Node Text="mimetypes" Leaf="true" />
          </Children>
        </ext:Node>
        <!-- all the other sizes go here -->
      </Children>
    </ext:Node>
  </Root>
</ext:TreePanel>

As a descendent class of Panel, you can set properties such as Title, AutoScroll, DockedItems, and more. By default, leaf node icons are different from non-leaf nodes. These can be set via the Icon or IconCls property as we have seen throughout Ext.NET. You can also hide the root node by setting RootVisible="false" on the TreePanel. These two techniques were used to produce the TreePanel on the right, shown earlier.

In this next example, the nodes are populated by reading the directory structure recursively from the filesystem. The following is a method we will invoke from code-behind to create the tree nodes:

public Node CreateTreeNode(string path)
{
  int lastSeparatorPos = 
      path.LastIndexOf(Path.DirectorySeparatorChar) + 1;
  string directoryName = path.Substring(lastSeparatorPos);

  var dirs = new DirectoryInfo(path).GetDirectories()
    .OrderBy(info => info.Name, new NaturalOrderComparer());

  var node = new Node
  {
    Text = directoryName,
    Leaf = !dirs.Any()
  };

  foreach (DirectoryInfo subDir in dirs)
  {
    node.Children.Add(CreateTreeNode(subDir.FullName));
  }

  return node;
}

The preceding method works by creating a node for the current path. All its subdirectories are added to it by recursively calling itself.

Note

.NET's implementation does not sort directories in the same name order as Windows Explorer does (as can be seen in the earlier screenshot). A custom NaturalOrderComparer has, therefore, been used, but its implementation is not important for the purposes of this example.

We can then modify our initial code example. The markup now looks like this:

<ext:TreePanel ID="IconFolderTree" runat="server" Width="250" 
  Height="250" Icon="Pictures" Title="Icons" AutoScroll="true"
  />

Note, we explicitly set the TreePanel's ID, so we can refer to it in the code-behind:

protected void Page_Load(object sender, EventArgs e)
{
  Node rootNode = CreateTreeNode(StartingPath);
  rootNode.Expanded = true;
  rootNode.Children[0].Expanded = true;
  this.IconFolderTree.Root.Add(rootNode);
}

We assume StartingPath has been set to the location of the images. Then it simply sets the root node of the TreePanel to be the result of the CreateTreeNode method we defined earlier. The end result is the same as the earlier screenshot.

Asynchronous tree node loading

In the earlier examples the tree nodes are all loaded up front. In some cases this is not practical; it may be too much to load up front for the server or just a waste if many nodes are not going to be opened. We may, therefore, want to create the tree nodes on demand when the user (or other code) opens the tree.

TreePanel supports remote loading by reusing the Store architecture. In addition, nodes created asynchronously are just normal tree nodes so closing/expanding them again does not require another call to the server, when they have already been fetched.

Consider the following modification of our earlier example to show the Store approach:

<ext:TreePanel ID="IconFolderTree" runat="server" Width="250" Height="250" Icon="Pictures" Title="Icons" AutoScroll="true">
  <Root>
    <ext:Node NodeID="Root" Text="icons" Expanded="true" />
  </Root>
  <Store>
    <ext:TreeStore runat="server">
      <Proxy>
        <ext:AjaxProxy Url="/IconTree/GetSubDirectories"/>
      </Proxy>
    </ext:TreeStore>
  </Store>
</ext:TreePanel>

In the preceding code snippet, the following are worth looking at further:

  • The NodeID property for the root node and for asynchronous requests

  • The use of a TreeStore to create nodes on the server

  • The implications of Expanded="true"

NodeID for asynchronous node requests

The server needs an identifier for the node being expanded so it can get the appropriate data. That is defined by the NodeID property and is, therefore, required for remote loading.

In our example, each node is a directory path, so we could just use the full file path as it will be unique. However, we don't want to leak the full path to the outside world. So we will just use "Root" as the starting point, as we know it will not be the name of any child folders. The server can map that to the actual file path.

Using TreeStore to create nodes on the server

TreeStore is a subclass of AbstractStore, which is a base class for the Store as well. Hence, we get to reuse the same Proxy mechanisms we have seen earlier for other data-bound items. We will cover TreeStore a bit later to see other data-related features.

In this example, we have opted to call an ASP.NET MVC Controller:

//
// GET: /IconTree/GetSubDirectories
//
public StoreResult GetSubDirectories(string node)
{
  var path = node == "Root"
    ? StartingPath
    : Path.Combine(StartingPath, node);

  var dirs = new DirectoryInfo(path).GetDirectories()
    .OrderBy(info => info.Name, new NaturalOrderComparer());

  var nodes = new NodeCollection(false);

  foreach (DirectoryInfo subDir in dirs)
  {
    var childNode = new Node
    {
      NodeID = subDir.FullName.Substring(StartingPath.Length + 1),
      Text   = subDir.Name,
      Leaf   = subDir.GetDirectories().Length == 0
    };
    nodes.Add(childNode);
  }

  Return this.Store(nodes); 
}

The preceding controller has code similar to the earlier CreateTreeNode method, but we have taken out the recursion as we are now creating nodes on demand.

The node parameter contains the value of the node ID. The first line checks that if the node ID passed in is the root ("Root"), it will use the internally known StartingPath. Otherwise, they are combined. When looping through the subdirectories, new nodes are created with node IDs so we also strip out the StartingPath of the path there.

Note the controller is returning an instance of StoreResult instead of the typical JsonResult, which is normally used for a JSON result.

Expanding tree nodes

A node can be set to appear expanded or collapsed, via the Expanded property. The TreePanel just invokes the expand method internally if needed. Therefore, if remote loading is enabled and Expanded is true for the root node, it will automatically make the AJAX request to fetch its immediate child nodes for you when the tree is rendered.

For the root node, Expanded="true" has been set to expand just the next level. You can also hide the root node itself by using RootVisible="false". This also has the effect of forcing the root node to be expanded. This can be changed by setting the AutoLoad property on the TreeStore to false.

In our controller we do not set the Expanded property on any of the generated nodes (so it will be false by default). If we did set any of those nodes to Expanded="true" and they were not initialized with child nodes already, they too would automatically load their child nodes via additional AJAX requests when added to the tree. If nodes were set to be expanded in our preceding code, it would mean you would actually recursively open all nodes. This would not be an optimal way to load all nodes because of the excessive number of AJAX requests. The earlier examples would be better for this purpose, instead. However, if you know some conditions when to auto-expand and when not to, in your server code, then this could be a useful trick.

Data binding with TreeStore and ColumnModel

TreePanel is quite rich as it inherits from TablePanel, which is what GridPanel inherits from, while TreeStore inherits from Store. This makes data binding familiar, but it also allows us to use multiple fields and columns to represent additional data.

Custom node attributes and explicit TreeStore Models

You may want to store an additional state on each node. In our earlier example, we only have the node's ID holding some path information and the text representing the path name. Suppose when a tree's node is clicked, we will show the number of files in that directory, the number of subdirectories, and when that directory was last modified. This can be done by adding additional data to each node's CustomAttributes collection. Each item in the collection is of the type ConfigItem, which we have seen before.

We then need the tree's store to know about these additional items. The TreeStore examples up to now have not defined any fields as seen with other Stores, so an implicit model is created internally. By defining fields explicitly, the tree and its TreeStore will map the custom attributes to the correct fields for you, as follows:

<ext:TreeStore runat="server">
  <Model>
    <ext:Model>
      <Fields>
        <ext:ModelField Name="numFiles" Type="Int" />
        <ext:ModelField Name="numSubDirs" Type="Int" />
        <ext:ModelField Name="lastMod" Type="Date" 
                        DateFormat="yyyy-MM-dd HH:mm:ss" />
      </Fields>
    </ext:Model>
  </Model>
  <Proxy>
    <ext:AjaxProxy Url="/IconTree/GetSubDirectories" />
  </Proxy>
</ext:TreeStore>

Note the node ID itself doesn't need mapping; it is mapped to the Store Model's IDProperty, which is id by default (and a Node's NodeID serializes to id). Also note that TreePanel has a Fields property, which is shorthand for the Fields collection in the TreeStore Model.

To show additional information when a node is clicked we will add a static helper method to the IconTreeController which will add the custom attributes as follows:

public static void SetNodeCustomAttributes(
  Node node, DirectoryInfo dirInfo)
{
  string lastMod = dirInfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss");
  string numFiles = dirInfo.GetFiles().Length.ToString();
  string numSubDirs = dirInfo.GetDirectories().Length .ToString();

  var attrs = new[]
  {
    new ConfigItem("lastMod", lastMod, ParameterMode.Value),
    new ConfigItem("numFiles", numFiles, ParameterMode.Raw),
    new ConfigItem("numSubDirs", numSubDirs, ParameterMode.Raw)
  };
  node.CustomAttributes.AddRange(attrs);
}

A shorthand to the CustomAttributes collection is to use AttributesObject:

node.AttributesObject = new { lastMod, numFiles, numSubDirs };

The controller's GetSubDirectories method calls it before adding the child nodes:

SetNodeCustomAttributes(childNode, subDir);
nodes.Add(childNode);

In addition we will also call that method for the root node of the tree:

protected void Page_Load(object sender, EventArgs e)
{
  if (!X.IsAjaxRequest)
  {
    var dirInfo = new DirectoryInfo(StartingPath);
    var rootNode = this.IconFolderTree.Root[0];
    this.IconTreeController.SetNodeCustomAttributes(rootNode,
                                                    dirInfo);
  }
}

Next, we add a panel to the docked item of the tree and handle the node selection event:

<ext:TreePanel ID="IconFolderTree" runat="server" Width="250" Height="250" Icon="Pictures" Title="Icons" AutoScroll="true">
  <!-- Root and Store declared as before -->
  <DockedItems>
    <ext:Panel Dock="bottom" Height="70">
      <Tpl>
        <Html>
          <p>Last Modified: <strong>{lastMod}</strong></p>
          <p>Number of files: <strong>{numFiles}</strong></p>
          <p>Number of subdirectories: <strong>{numSubDirs}</strong></p>
        </Html>
      </Tpl>
    </ext:Panel>
  </DockedItems>
  <Listeners>
    <Select Handler="MyApp.showInfo.call(
      #{IconFolderTree}, record);" />
  </Listeners>
</ext:TreePanel>

The select handler updates the docked panel with the record data for the current node:

var MyApp = {
  showInfo: function (record) {
    var panel = this.getDockedItems("panel[dock=bottom]")[0];
    panel.update(record.data);
  }
};

When all of that is put together, you will see the following once a tree node is selected:

Multiple fields and tree grids

Like the GridPanel, the TreePanel also supports a column model because they both inherit the abstract TablePanel. This enables another type of tree-based UI, the tree grid, whereby you can show additional columns of data for each node. By default a TreePanel uses a single column. However, we can explicitly add columns:

<ext:TreePanel ID="IconFolderTree"
  runat="server" Width="300" Height="250"
  Icon="Pictures" Title="Icons" AutoScroll="true">
  <ColumnModel>
    <Columns>
      <ext:TreeColumn Text="Folder" Flex="1" DataIndex="text" />
      <ext:TemplateColumn Text="Last Modified" Width="150" 
        DataIndex="lastMod">
        <Template>
          <Html>
            {lastMod:date("F j, Y, g:i a")}
          </Html>
        </Template>
      </ext:TemplateColumn>
    </Columns>
  </ColumnModel>
  <!-- everything else as before -->
</ext:TreePanel>

We have added two columns to our TreePanel, TreeColumn and TemplateColumn. TreeColumn provides the indentation and folder structure which is required for any tree grid approach. The other column is a regular column that we also see on grids. Note for TreeColumn, the DataIndex property has been set to "text". Like id, this is one of the default properties for a given store record of a node. The text value is the display text for each node. This gives us the following screenshot:

What else can you do with TreePanels?

It is highly recommended to visit the Ext.NET Examples Explorer to see features such as, filtering, checkbox nodes, editing nodes, submitting selected nodes, reordering nodes, drag-and-drop between trees and other components, loading an XML file, and more:

http://examples.ext.net/#/search/tree