Form Browser Visual Studio Add-In

March 23, 2013 / / 0 comments


Visual Studio Add-Ins are small application extensions developed to improve the user experience from the IDE. I have been always curious how it is possible to create an external tool which has the ability to cooperate with Visual Studio's environment. I believed the extensions are often full of hacky and non-standard solutions. Development of an Add-in was surely far from my confort zone, so I've decided to take a deep dive to this area. The number of the Visual Studio Add-In articles on the web is not really astonishing, that's why I think you will find this article useful. It's related to an Add-In which displays the Forms bound to the particular projects in a clear tree structure, similar to the Solution Explorer. You can find the complete Add-In with the code at the bottom of this page. This project was written in C++ CLI, but I believe the principle should be valid also for VB.Net or C#.

How to create and embed an Add-In into Visual Studio

An Add-In is a dynamic library which is loaded by the Visual Studio Environment. Basically we are able to configure our Add-In from scratch, but it is easier to use the Add-In project wizzard.


When the wizzard is finished, practically we have a ready to use Add-In. Afterwards we just need to solve when and how will our Add-In start. There are 4 important overloaded methods waiting for implementation:

  • OnConnection - Initialization of the Add-In comes here (custom Menu or ToolBar Buttons, etc.)
    void Connect::OnConnection(Object^ Application, ext_ConnectMode ConnectMode, Object^ AddInInst, Array^% custom)
    {
       appObject = dynamic_cast<DTE2^>(Application);
       addInInstance = dynamic_cast<AddIn^>(AddInInst);

       // Initialize the add-in when VS is setting up it's user interface
       if (ext_ConnectMode::ext_cm_UISetup==ConnectMode)
          InitializeToolBarButton();
    }

  • OnDisconnection - Release the acquired resources here
    void Connect::OnDisconnection(ext_DisconnectMode removeMode, Array^% custom)
    {
       try
       {
          switch (removeMode)
          {
          case ext_DisconnectMode::ext_dm_HostShutdown:
          case ext_DisconnectMode::ext_dm_UserClosed:

             // remove the Command Bar button from the ToolBar
             if (nullptr != StdCommandBarButton)
                StdCommandBarButton->Delete(true);

             // release the selection event
             if (nullptr != MonitorSelection && 0 != SelectionCookie)
                MonitorSelection->UnadviseSelectionEvents(SelectionCookie);

             // unbind from the project property changed events
             UnbindFromProjectPoropertyChangedEvents();

             break;
          }
       }
       catch (Exception^ e)
       {
          MessageBox::Show(e->ToString());
       }
    }

  • Exec - This method is called when the Add-In is executed (user clicked onto the ToolBar, etc.)
    void Connect::Exec(String^ CmdName, vsCommandExecOption ExecuteOption, Object^% VariantIn, Object^% VariantOut, bool% handled)
    {
       handled = false;

       if (vsCommandExecOption::vsCommandExecOptionDoDefault == ExecuteOption)
       {
          // when the ToolBar Button is clicked through the UI
          if (!CmdName->CompareTo(AddInCmdName))
          {
             ShowAddInWindow();
             handled = true;
             return;
          }
       }
    }

  • QueryStatus - The IDE calls this method regularly to check the Add-In status (enabled/disabled)
    void Connect::QueryStatus(String^ CmdName, vsCommandStatusTextWanted NeededText, vsCommandStatus% StatusOption, Object^% CommandText)
    {
       // executed when the UI needs to check, whether
       // the ToolBar button is enabled or disabled
       if (vsCommandStatusTextWanted::vsCommandStatusTextWantedNone == NeededText)
       {
          if (!CmdName->CompareTo(AddInCmdName))
          {
             StatusOption = vsCommandStatus::vsCommandStatusSupported +
                            vsCommandStatus::vsCommandStatusEnabled;
          }
          else
          {
             StatusOption = vsCommandStatus::vsCommandStatusUnsupported;
          }
       }
    }

When I have started to work on this project I realized the Visual Studio's user interface offers really wide range of options for customizations. It is possible to bind our Add-In button (if we need to) at any place we want. c in the Menu or ToolBar is absolutely no problem. In the next example you can see how to bind a command (button) to the end of the Standard ToolBar.


void Connect::InitializeToolBarButton()
{
   try
   {
      Command^ command = nullptr;
      try
      {  // obtain the command if it is already created
         command = appObject->Commands->Item(addInInstance->ProgID + "." + AddInName, -1);
      }
      catch(Exception^){}

      // create the command if does not exists
      if (nullptr == command)
      {
         // it is better to use the newest Commands2, because it allows to create
         // the ToolBar button with the style definition at once
         EnvDTE80::Commands2^ commands2 = safe_cast<EnvDTE80::Commands2^>(appObject->Commands);

         // optional, determines which environment contexts (debug mode, design mode, ...) show the command
         Array^ contextGUIDs = Array::CreateInstance(Object::typeid, 0);

         // create the ToolBar button for our Add-In
         command = commands2->AddNamedCommand2(addInInstance,
            AddInName, AddInCaption, AddInToolTip, true, 59, contextGUIDs,
            (int)(vsCommandStatus::vsCommandStatusSupported | vsCommandStatus::vsCommandStatusEnabled),
            (int)vsCommandStyle::vsCommandStylePict, vsCommandControlType::vsCommandControlTypeButton);
      }

      // Obtain the Standard command bar and insert our ToolBar button there
      VisualStudio::CommandBars::CommandBars^ commandBars;
      commandBars = (VisualStudio::CommandBars::CommandBars^)appObject->CommandBars;
      CommandBar^ stdCommandBar = commandBars["Standard"];
      StdCommandBarButton = (CommandBarButton^)command->AddControl(stdCommandBar, stdCommandBar->Controls->Count+1);
   }
   catch(Exception^ e)
   {
      MessageBox::Show(e->ToString());
   }
}

The methods above represents the connection between the Visual Studio IDE and the Add-In itself. Nothing complicated... :) Now we can focus onto the core functionality of our Add-In.

How to create a Visual Studio style tool window

For our visual Add-In we will need a hosting window. We can achieve this easily by creating a System::Windows::Forms::Form which will contain our set of controls. Nevertheless, there is absolutely no problem with abouve solution , but we want more professional look. My opinion is, that an Add-In looks good, when the user is not able to determine whether it is the part of the environment or not. Visual Studio has a built in advanced dockable window system.


It is possible to obtain Visual Studio style dockable window easily through the CreateToolWindow2(...) method. We just need to define what will this window host and this method returns a fully functional Visual Studio style tool window.

void Connect::ShowAddInWindow()
{
   try
   {
      if (nullptr == MainWindow)
      {
         // obtain the assembly of the TreeView
         String^ TreeViewFullName = "System.Windows.Forms.TreeView";
         String^ assembly = Reflection::Assembly::GetAssembly(System::Windows::Forms::TreeView::typeid)->Location;

         // create the VS style Tool Window
         Object^ UserCtrlObject = nullptr;
         EnvDTE80::Windows2^ win = (EnvDTE80::Windows2^)appObject->Windows;
         MainWindow = win->CreateToolWindow2(addInInstance, assembly, TreeViewFullName, AddInCaption, VSToolWindowGuid, UserCtrlObject);

         ...

         // set-up the tree view
         FormTreeView = (TreeView^)UserCtrlObject;
         FormTreeView->Font = gcnew Drawing::Font(FormTreeView->Font->FontFamily, 9);
         FormTreeView->ImageList = TreeViewImageList;
         FormTreeView->ImageKey = "";
         FormTreeView->NodeMouseDoubleClick += gcnew TreeNodeMouseClickEventHandler(this, &Connect::NodeDoubleClick);

         ...
      }

      ...

      MainWindow->Visible = true;
   }
   catch (Exception^ e)
   {
      MessageBox::Show(e->ToString());
   }
}

How to bind to the Solution and Project events

The Form Browser Add-In displays the forms across the projects. If something change in the project or in the solution we need to actualize our tree view. It is obvious we need to bind to the changes in the solution or in the projects. The Visual Studio environment provides an easy way how to bind to several common events. The mostly used events are available at one place, specifically in DTE2::Events. For more details check this place.

...

// bind to the solution events
SolEvents = appObject->Events->SolutionEvents;
SolEvents->Opened += gcnew _dispSolutionEvents_OpenedEventHandler(this, &Connect::SolutionOpened);
SolEvents->AfterClosing += gcnew _dispSolutionEvents_AfterClosingEventHandler(this, &Connect::SolutionAfterClosing);
SolEvents->Renamed += gcnew _dispSolutionEvents_RenamedEventHandler(this, &Connect::SolutionRenamed);
SolEvents->ProjectAdded += gcnew _dispSolutionEvents_ProjectAddedEventHandler(this, &Connect::SolutionProjectAdded);
SolEvents->ProjectRemoved += gcnew _dispSolutionEvents_ProjectRemovedEventHandler(this, &Connect::SolutionProjectRemoved);
SolEvents->ProjectRenamed += gcnew _dispSolutionEvents_ProjectRenamedEventHandler(this, &Connect::SolutionProjectRenamed);

// bind to the project events
ProjEvents = safe_cast<EnvDTE80::Events2^>(appObject->Events)->ProjectItemsEvents;
ProjEvents->ItemRemoved += gcnew _dispProjectItemsEvents_ItemRemovedEventHandler(this, &Connect::ProjectItemRemoved);
ProjEvents->ItemRenamed += gcnew _dispProjectItemsEvents_ItemRenamedEventHandler(this, &Connect::ProjectItemRenamed);

...

How to bind to the StartUp project changed event

In the Solution Explorer there is one special project called StartUp project. This project is marked bold. If we hit F5 (Start Debugging) the Visual Studio knows, this StartUp project should be executed. The solution and project events described in the previous section does not cover the special events like the StartUp project changed event. If we want to be notified that the StartUp project was changed we need to implement the IVsSelectionEvents interface. In this interface we can find the OnElementValueChanged(...) method which serves us as a callback method for the StartUp project changed event.

void Connect::ShowAddInWindow()
{

   ...

   // bind to the startup project changed event
   OLE::Interop::IServiceProvider^ SProvider = safe_cast<OLE::Interop::IServiceProvider^>(appObject);
   Guid MS_GuidService = (Guid)(IVsMonitorSelection::typeid)->GUID;
   Guid MS_riid = (Guid)(IVsMonitorSelection::typeid)->GUID;
   IntPtr MS_ppvObject;

   if (SProvider->QueryService(MS_GuidService, MS_riid, MS_ppvObject)==VSConstants::S_OK && IntPtr::Zero!=MS_ppvObject)
   {
      MonitorSelection = safe_cast<IVsMonitorSelection^>(Marshal::GetObjectForIUnknown(MS_ppvObject));
      MonitorSelection->AdviseSelectionEvents(this, SelectionCookie);
   }

   ...

}

...

int Connect::OnElementValueChanged(unsigned int elementid, Object^ varValueOld, Object^ varValueNew)
{
   // when the startup project is changed in the solution explorer
   if ((unsigned int)VSConstants::VSSELELEMID::SEID_StartupProject == elementid)
   {
      RefreshTreeView();
   }
   return VSConstants::S_OK;
}

How to determine whether a ProjectItem is a Form

Traversing through the Project item tree we can find several type of items. Mainly Cpp files, Header files, Resources, Directories and also Forms. The Form is basically a Header file with FileType="C++ Form". So, we have two possibilities how to determine whether the item is a Form. At first is to obtain the property of the ProjectItem and check the FileType property. This approach is fast, but there is also a disadvantage that the FileType needs to be set. The second solution is to traverse through the elements in the CodeElements source tree and find the class which was inherited from System::Windows::Forms::Form.

Boolean Connect::IsForm(EnvDTE::ProjectItem^ ProjectItem)
{
   if (!ProjectItem)
      return false;

   for each (Property^ prop in ProjectItem->Properties)
   {
      if ("FileType" == prop->Name)
      {
         VCProjectEngine::eFileType fType;
         fType = (VCProjectEngine::eFileType)prop->default;

         // we are looking for file of type CppForm
         if (VCProjectEngine::eFileType::eFileTypeCppForm==fType)
            return true;

         return false;
      }
   }

   return false;
}


Boolean Connect::IsForm(EnvDTE::CodeElements^ Elements)
{
   if (Elements==nullptr)
      return false;

   for each (CodeElement^ Element in Elements)
   {
      // Indicates if the code model element is located in the same project file.
      // This is important when attempting to navigate to a specific code element.
      if (Element->InfoLocation != vsCMInfoLocation::vsCMInfoLocationProject)
         continue;

      // Accept only Classes and Namespaces
      if (Element->Kind!=vsCMElement::vsCMElementClass && Element->Kind!=vsCMElement::vsCMElementNamespace)
         continue;

      // the element we are looking for is a Class, is inherited from System::Windows::Forms::Form
      if (Element->Kind==vsCMElement::vsCMElementClass)
      {
         CodeClass^ ClassElement = safe_cast<CodeClass^>(Element);
         CodeElements^ Bases = ClassElement->Bases;
         for each(CodeElement^ BaseElement in Bases)
         {
            if (BaseElement->FullName == "System::Windows::Forms::Form")
               return true;
         }
      }
      else
      {
         CodeNamespace^ NamespaceElement = safe_cast<CodeNamespace^>(Element);
         if (IsForm(NamespaceElement->Members))
            return true;
      }
   }

   return false;
}

How to bind to the Project Item property changed events

There are several Project and Solution events which notifies us when something has changed. All these events works well, except the ProjectEvent->ItemAdded. Although, we are notified about an item was added to the project, but we are not able to determine whether it is a form or not. When we try to check the FileType of the newly added item, it is recognized just as a Header file. At that time the file content is also empty, so checking the source code elements is also useless. In this case it is much better to bind to the Property changed events. If the FileType of the ProjectItem is changed we are notified about it and we can update our tree view.

void Connect::UnbindFromProjectPoropertyChangedEvents()
{
   if (!Hierarchies)
      return;

   for (int i=0; i<Hierarchies->Count; i++)
      delete Hierarchies[i];
}


void Connect::BindToProjectPropertyChangedEvents()
{
   // unbind the project property changed events first
   UnbindFromProjectPoropertyChangedEvents();

   // obtain the service provider
   OLE::Interop::IServiceProvider^ SProvider = safe_cast<OLE::Interop::IServiceProvider^>(appObject);
   Guid Sol_GuidService = (Guid)(SVsSolution::typeid)->GUID;
   Guid Sol_riid = (Guid)(SVsSolution::typeid)->GUID;
   IntPtr Sol_ppvObject;

   // obtain the solution object
   if (SProvider->QueryService(Sol_GuidService, Sol_riid, Sol_ppvObject)==VSConstants::S_OK && IntPtr::Zero!=Sol_ppvObject)
   {
      IVsSolution^ Sol = safe_cast<IVsSolution^>(Marshal::GetObjectForIUnknown(Sol_ppvObject));
      IEnumHierarchies^ EnumHierarchies = nullptr;
      Guid ProjectGUID = Guid(VSProjectGuid);

      // enumerate through the projects and bind the project changed events
      if (Sol->GetProjectEnum((unsigned int)(__VSENUMPROJFLAGS::EPF_MATCHTYPE | __VSENUMPROJFLAGS::EPF_ALLPROJECTS), ProjectGUID, EnumHierarchies)==VSConstants::S_OK && EnumHierarchies!=nullptr)
      {
         UInt32 pceltFetched;
         array<IVsHierarchy^>^ rgelt = gcnew array<IVsHierarchy^>(1){nullptr};
         for (EnumHierarchies->Reset(); EnumHierarchies->Next(1, rgelt, pceltFetched)==VSConstants::S_OK && pceltFetched==1; )
         {
            Hierarchy^ NewHierarchy = gcnew Hierarchy(rgelt[0]);
            NewHierarchy->PropertyChanged += gcnew Hierarchy::HierarchyEventHandler(this, &Connect::HierarchyPropertyChanged);
            Hierarchies->Add(NewHierarchy);
         }
      }
   }
}

How to install the Add-In into the Visual Studio environment

When an AddIn is created through the New Project Wizzard, two *.AddIn files are created. One is located in our Project directory and the second, called "Something - For Testing.AddIn" is saved directly into the C:\Users\YOUR_NAME\Documents\Visual Studio 2010\Addins\ folder. By the way, this is the palace what Visual Studio checks for Add-Ins during startup. The mentioned *.AddIn files contains the information we filled through the wizzard and also the location of our Add-In *.dll. If we want to install an Add-In manually, it is necessary to copy our *.AddIn file into the Addins folder and also set the path of the AddIn dll file.

History

2013/03/23 - 1.0 - First release with all features mentioned in the article

Downloads


0 comments:


Post a Comment