Dotneteers.net
All for .net, .net for all!

LearnVSXNow! Part #40 - Working with Hierarchies Part 5 - Managed Classes for Custom Hierarchies

In the previous part of the series I promised to create a code sample to illustrate how to create custom hierarchies. In this article I show a simple custom hierarchy that displays a root folder with its subfolders and files as child nodes.

If I had built my sample on the top of Managed Package Framework classes I would have used about 700 lines of code to implement the IVsUIHierarchy interface solving this task and about 80 lines of code to create a hierarchy window around COM implementation of Visual Studio. To be honest, I did it before but was very dissatisfied with the result because it was difficult to read and follow, and was not flexible at all.

If I had to create the same hierarchy using a Tree View control, I would need less than hundred lines of code.

Parallel with showing a sample I also would like to treat the steps of building custom hierarchies. So I decided first creating a pattern that is general enough to create most custom hierarchies, then creating a few sample leveraging on this pattern, and last treat how the pattern was implemented.

In this part I introduce you this blueprint and then show the sample with its—actually full—source code. In next parts I show you more and more samples and allow a closer look to the pattern implementation.

The VSXtra Hierarchy Pattern

Because working directly with IVsUIHierarchy is not easy, I decided to create managed classes for using hierarchies in a much easier and straightforward way. The MPFProj project inspired me, I used it to learn and discover the basics of Visual Studio hierarchies. I examined the pattern implemented there and decided to use another pattern that fits for VSXtra better. MPFProj is about creating a managed custom project system that is a kind of custom hierarchy but with special responsibilities expected by Visual Studio from a project system.

I used the following pattern to share the responsibilities among objects:

The central entity in this pattern is the HierarchyManager<TPackage> generic type which has the following responsibilities:

—  Keeps track a list of nodes that belong to the hierarchy represented by a manager instance (ManagedItems container).

—  Creates only the root node of the hierarchy (HierarchyRoot property), but lets the other nodes organize the hierarchy among themselves.

—  Can be attached to a node of another hierarchy and stated as a nested hierarchy. It can access the hosting hierarchy through the ParentHierarchy property.

—  Can be associated with a hierarchy window and access its hosting window through the UIHierarchyToolWindow property.

—  Accepts commands coming from the environment and decides how to handle those commands.

—  Provides image management support for the hierarchy to use icons for the hierarchy node. Allows centralized (the hierarchy manager declares the icons to be used) and decentralized (nodes can set the icons to use) approach.

The HierarchyNode abstract class represents a node within the hierarchy. A node itself is responsible to maintain its properties including the visual and structural properties. HierarchyNode is the class that can add or remove its children and access its parent node. Moreover, a node instance can access its manager instance through its ManagerNode property. In case of changes in its properties or in its structure, a HierarchyNode raises events to notify its subscribers.  HierachyNode also listens to the messages coming from the environment (for example commands addressed directly to a node, messages from the hierarchy window about the node is visually expanded or collapsed, etc.).

The UIHierarchyToolWindow<TPackage> managed class does exactly what its name suggest. It is a generic tool window hosting the IVsUIHierarchyWindow provided by Visual Studio. This class can host zero, one or more hierarchies through their HierarchyManager<> derived instances.

I think there is a need for some explanation about why using generic classes for HierarchyManager<> and UIHierarchyToolWindow<> and why not for HierarchyNode.

The two generic classes use the so-called package-scoped object pattern. They have a generic attribute to a PackageBase derived class. The pattern allows these classes to access the related package’s service providers directly without the need to pass a service provider instance explicitly. HierarchyNode does not need to access package level services, but in case it needs it can use its ManagerNode.

The BasicHierarchy Sample

I created a very simple application called BasicHierarchy sample to demonstrate the concepts of the hierarchy abstraction used in VSXtra. This hierarchy displays a root folder with its folders and files as child nodes as the following figure illustrates:

This sample uses the patterns introduced above and it contains the following essential types:

The sample program creates its own specialized hierarchy window called BasicHierarchyToolWindow<BasicHierarchyPackage> that uses the VSPackage class of the sample as its type parameter. This hierarchy is managed by the FileHierarchyManager<BasicHierarchyPackage> class. All nodes are related to this hierarchy, so an abstract FileHierarchyNode class is defined to be the root of this hierarchy. RootNode, FolderNode and FileNode are inherited from this abstract root class. It is the responsibility of the hierarchy manager class to create the root node of the hierarchy, so the FileHierarchyManager creates and sets up an instance of RootNode in this role.

To get a closer look about the implementation of this pattern, let’s see the full source code of essential classes in this sample.

The VSPackage class

The package encapsulating the BasicHierarchy functionality has a very simple declaration providing a command execution method for the function showing up the tool window hosting the hierarchy. It uses the command handler mechanism built into VSXtra:

[PackageRegistration(UseManagedResourcesOnly = true)]

[DefaultRegistryRoot("Software\\Microsoft\\VisualStudio\\9.0")]

[InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

[ProvideLoadKey("Standard", "1.0", "Basic Hierarchy", "DeepDiver", 1)]

[ProvideMenuResource(1000, 1)]

[XtraProvideToolWindow(typeof(BasicUIHierarchyToolWindow))]

[Guid(GuidList.guidBasicHierarchyPkgString)]

public sealed class BasicHierarchyPackage : PackageBase

{

  [CommandExecMethod]

  [CommandId(GuidList.guidBasicHierarchyCmdSetString,

    CmdIDs.cmdidShowBasicHierachyWIndow)]

  [ShowToolWindowAction(typeof(BasicUIHierarchyToolWindow))]

  private static void ShowBasicUIHierarchyWindow()

  {

  }

}

The BasicUIHierarchyToolWindow class

The BasicHierarchyToolWindow is responsible to provide the hierarchy window functionality. It derives from the UIHierarchyToolWindow<TPackage> generic class and modifies it with style attributes. The initial hierarchy is set by overriding the HierarchyManager property of the tool window retrieving a new instance of FileHierarchyManager.

[InitialCaption("Basic UI Hierarchy window")]

[BitmapResourceId(301)]

[LinesAtRoot]

[DoNotSortRootNodes]

public sealed class BasicUIHierarchyToolWindow:

  UIHierarchyToolWindow<BasicHierarchyPackage>

{

  protected override HierarchyManager<BasicHierarchyPackage> HierarchyManager

  {

    get { return new FileHierarchyManager("C:\\"); }

  }

}

The InitialCaption and BitmapResourceId attributes are common for all tool windows; they set up its visual properties. The LinesAtRoot and DoNotSortRootNodes attributes are recognized and handled only by hierarchy windows. The first turns on drawing lines and the expander icon for the root node. The second instructs the hierarchy tree to display the nodes in their physical order as they are stored in the hierarchy without resorting them.

The FileHierarchyManager class

Hierarchy items are individual entities keeping track with their own properties and knowing only their parent and child nodes. They do not try to co-ordinate any activities with the hierarchy window: that is the responsibility of the hierarchy manager object. In our sample this is the FileHierarchyManager class that is inherited from the HierarchyManager<TPackage> generic class. The class has two housekeeping methods (CreateHierarchyRoot and InitializeHierarchyRoot) and adds a functional method called ScanContent.

public sealed class FileHierarchyManager : HierarchyManager<BasicHierarchyPackage>

{

  private readonly string _FullPath;

 

  public FileHierarchyManager(string fullPath)

  {

    _FullPath = fullPath;

  }

 

  // --- Creates the root of the hierarchy

  protected override HierarchyNode CreateHierarchyRoot()

  {

    return new RootNode(null, _FullPath, _FullPath);

  }

 

  // --- Initializes the root node of the hierarchyafter it has been created

  protected override void InitializeHierarchyRoot(HierarchyNode root)

  {

    var fileRoot = root as FileHierarchyNode;

    if (fileRoot == null) return;

    ScanContent(fileRoot);

  }

 

  // --- Scans the content (folders and files) to create child nodes

  public void ScanContent(FileHierarchyNode node)

  {

    if (node == null) throw new ArgumentNullException("node");

    try

    {

      var path = node.FullPath;

      foreach (var dir in Directory.GetDirectories(node.FullPath))

      {

        var dirName = dir.Substring(path.Length + (path.EndsWith("\\") ? 0 : 1));

        node.AddChild(new FolderNode(this, dir, dirName));

      }

      foreach (var file in Directory.GetFiles(path))

      {

        var fileNode = new FileNode(this, file, Path.GetFileName(file));

        node.AddChild(fileNode);

      }

    }

    catch (SystemException)

    {

      // --- This exception is intentionally supressed.

    }

  }

}

The first time the hierarchy windows asks the manager class for the root item, a HierarchyNode-derived node instance is created by the CreateHierarchyRoot factory method. When the root node is sited in the manager class, the InitializeHierarchyRoot method is called to finish the setup of the root node. The ScanContent method can be used by any root nodes to discover the subfolders and files under an abstract FileHierarchyNode. In this sample only the root node uses this method, but it is placed here in order to be accessible by any hierarchy nodes. It is easy to access the method, because each node contains a reference to the hierarchy manager.

The FileHierarchyNode class

All nodes in this sample are inherited from the abstract FileHierarchyNode class that stores the FullPath string belonging to the concrete node. The RootNode, FolderNode and FileNode classes derive from this abstract class.

public abstract class FileHierarchyNode : HierarchyNode

{

  public FileHierarchyNode(FileHierarchyManager manager, string fullPath,

    string caption) : base(manager, caption)

  {

    FullPath = fullPath;

  }

 

  public string FullPath { get; private set; }

}

The RootNode class

The root if the hierarchy is represented by an instance of this class. This class is the only one containing child nodes in this simple hierarchy. As a design decision the root node does not create its own child nodes, this task is delegated to the hierarchy manager.

[HierarchyBitmap("HomeImage")]

public sealed class RootNode : FileHierarchyNode

{

  public RootNode(FileHierarchyManager manager, string fullPath, string caption) :

    base(manager, fullPath, caption)

  {

  }

}

The root hierarchy node sets up its own icon with the HierarchyBitmap attribute. The string value of the attribute is used by the hierarchy manager to associate the bitmap with the “HomeImage” resource name to the root node.

The FolderNode and FileNode classes

The two other node types are represented by the FolderNode and FileNode classes. They do not provide any new functionality but simply change the visual properties of the corresponding node instances.

[HierarchyBitmap("FolderImage")]

[SortPriority(20)]

public sealed class FolderNode : FileHierarchyNode

{

  public FolderNode(FileHierarchyManager manager, string fullPath, string caption)

    : base(manager, fullPath, caption)

  {

  }

}

 

[HierarchyBitmap("FileImage")]

[SortPriority(30)]

public sealed class FileNode : FileHierarchyNode

{

  public FileNode(FileHierarchyManager manager, string fullPath, string caption) :

    base(manager, fullPath, caption)

  {

  }

}

Both classes are decorated with the SortPriority attribute that are used by the manager when inserting a new child node. When a new child node is added to a parent it is inserted into its position determined by a sort order. Two nodes can be compared by their values and they can be said to be equal or one of the node is less than the other. During this comparison the SortPriority value is used. If two nodes have the same SortPriority, their Caption property values are compared as strings to determine which has the less value.

Setting the SortPriority attributes as above will result in ordering the child nodes so that all folder nodes will precede file nodes.

Where are we?

Instead of implementing IVsUIHierarchy for each custom hierarchy I created a generic pattern that can be used for majority of hierarchies. This pattern divides the responsibilities among the hierarchy window, the hierarchy manager and the hierarchy node in a clean practical fashion. Using this pattern it is really easy to create simple hierarchies as you could see from the source code above.


Posted Dec 05 2008, 03:17 PM by inovak
Filed under: ,

Comments

GuoHui Chen wrote re: LearnVSXNow! Part #40 - Working with Hierarchies Part 5 - Managed Classes for Custom Hierarchies
on Sun, Mar 8 2009 12:09

Thank you for your articals!

http://bestmedicineonline.info wrote re: LearnVSXNow! Part #40 - Working with Hierarchies Part 5 - Managed Classes for Custom Hierarchies
on Fri, Feb 15 2013 21:15

xOFr5J I really liked your article.Much thanks again. Keep writing.