Mark Miller
  • Talk about CodeRush and Refactor! Pro

    Noticed these blog posts recently:

    Seems like we're seeing more and more plug-ins for CodeRush and Refactor! Pro. Thanks to everyone who's helping grow the community.

  • New CodeRush Plug-in: Jump to Implementer

    I received a tech support question yesterday asking if there was an easy way to find all the implementers of a particular interface. While CodeRush has a nice Find All References window and a neat Tab to Next Reference feature, it doesn't filter out references to the interface that are used in variable declarations and expressions. So as a result both of those approaches produce more noise than the customer wanted. There is also a Jump to Implementers feature available when you right-click an interface declaration and choose Jump to..., however that feature requires that the caret be on the interface declaration, and is not available on interface references elsewhere in the code.

    Fortunately the DXCore framework that CodeRush, Refactor! Pro, and a host of powerful third-party plug-ins are built upon makes adding this feature very easy.

    And that's what we'll build right now. This plug-in is going to rely upon functionality that ships with CodeRush, specifically the Jump To menu which is available though a right-click in the editor.

    The steps listed here assume you have CodeRush and Refactor! Pro installed.

    Creating the Plug-in Project

    Inside Visual Studio, from the DevExpress menu, select "New Plug-in...". If you don't have a DevExpress menu or you don't see a "New Plug-in..." menu item, contact support@devexpress.com.

    NewDXCorePlugInProject 

    Specify the name of the project and the solution. I'm calling both the project and the solution "CR_JumpToImplementer". Click OK.

    The DXCore Plug-in Project Settings dialog appears.

    DXCorePluginProjectSettings

    Click OK to accept the default settings.

    Next the plug-in project will open in Visual Studio.

    Working with the NavigationProvider

    Since we're adding a navigation feature, and we want it to be part of the Jump to menu, we need to drop a NavigationProvider onto the form. The NavigationProvider is a relatively new control, and so it may not appear on the DXCore page of the Toolbox. If you don't see it, activate the DXCore control page, right-click the ToolBox and select "Choose Items..."

    ChooseItems

    In the Filter TextBox enter "navigation" and then place a checkmark next to the NavigationProvider entry.

    AddNavigationProviderToToolBox

    And click OK. Now you'll have the NavigationProvider control on your DXCore page of the ToolBox.

    Click the NavigationProvider control...

    ClickNavigationProvider

    and then click the design surface to add one of these to your plug-in.

    NavigationProviderDropped

    NavigationProvider Properties

    Property Value
    (Name) navJumpToImplementer
    Description Jumps to the selected implementer of this interface.
    DisplayName Interface Implementer
    ProviderName InterfaceImplementer

    The ProviderName identifies our NavigationProvider to the DXCore, and the DisplayName tells the DXCore the text to display in the Jump to menu. The Description text is placed in a hint that explains what will happen when this menu item is selected.

    NavProvProperties

    NavigationProvider Events

    Next, on the Properties grid click the Events button.

    ClickEvents

    There are two important events we'll need to handle. The first is CheckAvailability, which gives us a chance to find out if our Jump to Implementer navigation feature should be shown in the menu. In our handler for this event, we can check to see if the token at the caret is a reference to an interface.

    CheckAvailability

    Double-click the CheckAvailability event to generate a handler for it.

    HandleCheckAvailability

    Next, we'll write code to determine if the active element (the identifier or reference at the caret) is a reference to an interface. When I need to write code like this in a plug-in, I always create some sample code first that represents the code I want the plug-in to work on, then I move the caret through that sample code and see what it looks like in the Expressions Lab.

    The Expressions Lab is a DXCore plug-in that reveals the structure of the code in the editor. It's a direct view into the language-independent parse tree that the DXCore generates with its background compilers. If you don't have the Expressions Lab installed yet, you can download a copy and learn more about it here.

    Note: You don't need to install the Expressions Lab to finish writing the plug-in, but it is useful in exploring parse trees and especially comes in handy if you want to write plug-ins that work with source code.

    After installing (you will need to restart Visual Studio), bring up the Expressions Lab from the DevExpress menu by selecting Tool Windows | Diagnostics | Expressions Lab.

    SelectExpressionsLab

    Next, let's write some simple code to explore. I started with this (added to the end of my Plugin1.cs file):

    public interface IMyInterface
    {
    }

    public class MyClass :
    IMyInterface
    {
    }
     

    Placing the caret on the second reference to IMyInterface (the one after MyClass) yields the following:

    InterfaceExpression

    The Expressions Lab shows the value of this element's ElementType property is LanguageElementType.TypeReferenceExpression. We can use this information to start building the code that determines if the active element (the element at the caret) is a reference to an interface. Although ElementType is an enum property, the names of its elements correspond to actual class names inside the DevExpress.CodeRush.StructuralParser namespace. For example, the TypeReferenceExpression is a class that represents a reference to a type (e.g., a class, interface, struct, etc.). The ElementType property is provided on all LanguageElements for performance and other technical reasons.

    A few other things we'll need to know before writing this code:

    • All LanguageElements have a GetDeclaration method. This returns an IElement representing the declaration.
    • The ElementType of an interface declaration is LanguageElementType.Interface. You can verify this in the Expressions Lab by moving the caret onto the IMyInterface declaration.

    So armed with this knowledge, our CheckAvailability event handler looks like this:

    private void navJumpToImplementer_CheckAvailability(object sender, CheckContentAvailabilityEventArgs ea)
    {
      if (ea.Element == null
    )
        return
    ;
      ea.Available = GetInterfaceDeclaration(ea.Element) != null
    ;
    }

    Here we're simply setting the Available property to true or false based on whether we've found an interface declaration for the active element (ea.Element). GetInterfaceDeclaration looks like this:

    private static IElement GetInterfaceDeclaration(LanguageElement element)
    {
      IElement
    declaration;

      if (element.ElementType == LanguageElementType
    .TypeReferenceExpression)
        declaration = element.GetDeclaration();
     
    else  // We might be on an interface declaration....
       
    declaration = element;

      if (declaration != null && declaration.ElementType == LanguageElementType
    .Interface)
        return
    declaration;

      return null
    ;
    }

    This code checks to see if the ElementType is a type reference, and if so finds the declaration for that interface. If the type of the active element is not a type reference, there is a chance that the type is an Interface, and so the active element is assigned to the declaration local variable. The code in the second if clause checks to see if the declaration variable is in fact a declaration for an Interface. And so if the element passed in is an interface or a reference to an interface, this method will return the declaration of that interface.

    So far so good. If we were to run our plug-in right now and test it, we could right-click on interfaces and references to interfaces, select "Jump To...", and then see our "Interface Implementer" menu item. Before we test, however, let's write some code to respond to the menu item being selected....

    As previously mentioned, there are two events that you must handle when creating a NavigationProvider. The first event is CheckAvailability, which tells the DXCore if our NavigationProvider's Display Text should appear as a menu item in the Jump to menu. The second event we need to handle is Navigate. This event is called when our menu item is selected.

    Navigate

    The Navigate event handler is where I want to find all the interface implementers and present those in a window, allowing the developer to select a desired implementer to jump to. So let's add the Navigate event handler.

    Activate the Plugin1.cs [Design] surface. Select the navJumpToImplementer control. In the Properties grid (on the Events page) double-click the Navigate event to generate a handler.

    HandleNavigateEvent

    There are two things we need to know before we write this code:

    • When we call GetDeclaration on a reference to a type, (as opposed to a reference to a variable or member), the result will be an ITypeElement.
    • ITypeElement has method called GetDescendants, which returns an array of ITypeElements that descend from or implement that type.

    Knowing this, we can start with code like the following:

    private void navJumpToImplementer_Navigate(object sender, DevExpress.CodeRush.Library.NavigationEventArgs ea)
    {
      IElement
    declaration = GetInterfaceDeclaration(ea.Element);
      ITypeElement iTypeElement = declaration as ITypeElement
    ;
      if (iTypeElement != null
    )
      {
        ITypeElement
    [] directImplementers = iTypeElement.GetDescendants();
        if (directImplementers != null && directImplementers.Length > 0
    )
        {
         
    // TODO: Create the form to show implementers of this interface.
        }
       
    else
          MessageBox.Show("There are no implementers of the X interface in this solution.");
      }
    }

    In the code above we call our GetInterfaceDeclaration method built earlier, and attempt to cast the result to an ITypeElement. If the cast is successful, we call the GetDescendants method to get a list of all the implementers of this interface.

    Notice the string passed to MessageBox.Show is incomplete. that was done intentionally so you could try a cool refactoring we use whenever we need to create strings with substituted parts. The refactoring is called Introduce Format Item.

    Select the "X" inside that string, and press the CodeRush/Refactor! key (Ctrl+`). You'll see something like this:

    IntroduceFormatItem

    Press Enter to apply the refactoring, and you'll get a new String.Format call. All you need to do is replace the "X" string with declaration.Name, like this:

    MessageBox.Show(String.Format("There are no implementers of the {0} interface in this solution.", declaration.Name));

    Introduce Format Item is the most efficient way to create strings with substituted parts.

    Next, let's create the UI for selecting the implementer to jump to.

    Selecting an Implementer

    In the Solution Explorer, right-click the project and select Add | "Windows Form...".

    AddWindowsForm

    Give this form a meaningful name (like "FrmPickDescendant" -- I'm thinking we might list more than just interface implementers here):

    AddDescendantPicker

    Click "Add".

    With the new form active in the designer, change the following values in the Properties grid:

    Property Value
    MinimizeBox False
    ShowInTaskbar False
    Text Select an Implementer

    While we start with a flat list of all interface implementers, I'm thinking about eventuall showing a full hierarchy, one that shows descendents of the classes that implement the interface (the descendants inherit the implementation so they too are implementers). I want to use a TreeView instead of a ListBox to show the items. So drop a TreeView onto the new form.

    SelectTreeView

    Click the TreeView's smart tag and click the Dock in parent container task.

    TreeViewSmartTag

    This will dock the TreeView entirely inside the form.

    DockedTreeView

    Change the following properties:

    Property Value
    (Name) trvDescendants

    Back in Plugin1.cs, let's add the following code inside our Navigate event handler:

    private void navJumpToImplementer_Navigate(object sender, DevExpress.CodeRush.Library.NavigationEventArgs ea)
    {
      IElement declaration = GetInterfaceDeclaration(ea.Element);
      ITypeElement iTypeElement = declaration as ITypeElement;
      if (iTypeElement != null)
      {
        ITypeElement[] directImplementers = iTypeElement.GetDescendants();
        if (directImplementers != null && directImplementers.Length > 0)
        {

          FrmPickDescendant frmPickDescendant = new FrmPickDescendant(directImplementers);
          if (frmPickDescendant.ShowDialog(CodeRush.IDE) == DialogResult.OK)
          {
           
    // TODO: Jump to the selected Interface implementer.
         
    }
        }
       
    else
          MessageBox.Show(String.Format("There are no implementers of the {0} interface in this solution."
    , declaration.Name));
      }
    }

    This code creates a new instance of the FrmPickDescendant class, passing the array of ITypeElements to its constructor. FrmPickDescendant does not yet have a constructor matching this signature, so place the caret on this constructor call, press the CodeRush/Refactor! key (Ctrl+`), and select Declare Constructor, like this:

    DeclareConstructor

    Using the target picker, select a location for the new constructor and press Enter.

    SelectConstructorLocation

    The construction code should look something like this:

    ConstructorAdded

    Press the copy key (Ctrl+C or Ctrl+Insert) to copy the directImplementers parameter to the clipboard. Press Enter to accept this suggested parameter name and move the caret onto the first line inside the constructor. Remove that throw statement if it exists (versions of CodeRush later than 3.2 will not by default add this throw statement to newly-built constructors).

    Next we want to iterate through all the passed-in implementers and add each one to the TreeView. So we need a foreach statement. CodeRush includes a template to make this easy, and this template works with identifiers on the clipboard (we have directImplementers on the clipboard right now). So on that same empty line enter "fe"....

    EnterFeTemplate

    Then press the space bar to expand that template. This will produce the following:

    AfterForEachExpansion

    Notice that ITypeElement in the screen shots above is not formatted as a class. We need add a namespace reference to the file.

    Place the caret on the ITypeElement reference, bring up the smart tag, and choose "using DevExpress.CodeRush.StructuralParser;".

    UsingStructuralParser

    Now inside this foreach loop, add the following line of code:

    trvDescendants.Nodes.Add(new TreeNode(iTypeElement.Name));

    This will create a new node and set its Text property to the name of each type. Note however that in a complex solution, I may have two or more types with the same name, living in different namespaces. I like the simplicity of listing the types without their corresponding namespace, so we'll need some way to jump to the selected implementer. So for now let's use the Tag property of the TreeNode to store a reference to the implementing element.

    With the caret somewhere on on the new TreeNode call, use CodeRush's Selection Increase feature (Num+ or Ctrl+Shift+W) to select the entire "new TreeNode(iTypeElement.Name)" expression....

    NewTreeNodeSelected

    Press the CodeRush/Refactor! key (Ctrl+`).

    IntroduceLocal

    Select Introduce Local and press Enter.

    Accept the default name of "newTreeNode" (you can press Enter to break the green links if you like), and add the following line to assign the iTypeElement reference to the TreeNode's Tag:

    foreach (ITypeElement iTypeElement in directImplementers)
    {
      TreeNode newTreeNode = new TreeNode
    (iTypeElement.Name);
      newTreeNode.Tag = iTypeElement;
      trvDescendants.Nodes.Add(newTreeNode);
    }
     

    Since we added a custom constructor to this Form, we need to make sure we get a call to InitializeComponent in, otherwise the reference to trvDescendants will be null when this code executes. So lets add a call to ": this()" at the end of this new constructor, like this (note -- versions of CodeRush after 3.2 are likely to add this call automatically):

    public FrmPickDescendant(ITypeElement[] directImplementers): this()
    {
     
    foreach (ITypeElement iTypeElement in directImplementers)
      {
        TreeNode newTreeNode = new TreeNode
    (iTypeElement.Name);
        newTreeNode.Tag = iTypeElement;
        trvDescendants.Nodes.Add(newTreeNode);
      }
    }

    There's one more feature I want to add to this form, and that's the ability to respond to Enter and Escape to accept or cancel the destination. So let's add the following method (parts borrowed from the Clipboard History plug-in):

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
      const int WM_KEYDOWN = 0x100
    ;
      if
    (msg.Msg == WM_KEYDOWN)
      {
        switch
    (keyData)
        {
          case Keys
    .Enter:
            this.DialogResult = DialogResult
    .OK;
            return true
    ;

          case Keys
    .Escape:
            this.DialogResult = DialogResult
    .Cancel;
            return true
    ;
        }
      }
      return base.ProcessCmdKey(ref
    msg, keyData);
    }
     

    Testing

    Now let's run and test what we have so far. From the Debug menu, select Start Debugging. A second instance of Visual Studio will launch. To test this, either open a solution that contains classes that implement interfaces, or create your own. I'm using the following simple sample code to test:

    namespace TestApp1
    {
      public interface
    IPerson
     
    {
        string Name { get; set
    ; }
        decimal Salary { get; set
    ; }
        void AdjustSalary(decimal
    amount);
      }

      public class Person :
    IPerson
      {
        public string Name { get; set
    ; }
        public decimal Salary { get; set
    ; }
        public void AdjustSalary(decimal
    amount)
        {
          Salary += amount;
        }
      }
    }

    Now, right-click one of the IPerson references in the code, and select "Jump to..."

    SelectJumpTo

    The Jump to menu will appear.

    JumpToInterfaceImplementer

    Select "Interface Implementer". Our "Select an Implementer" form will appear.

    SelectAnImplementer

    OK, so there's clearly some UI work to be done to improve the experience.  However the desired behavior (at least what we've coded so far) appears to be working. Let's make the test code a bit more complex by changing it to the following: 

    namespace TestApp1
    {
      public interface
    IPerson
     
    {
        string Name { get; set
    ; }
        int Age { get; set
    ; }
      }

      public class Person :
    IPerson
     
    {
        public string Name { get; set
    ; }
        public int Age { get; set
    ; }
      }

      public class Child:
    IPerson
     
    {
        public string Name { get; set
    ; }
        public int Age { get; set
    ; }
      }

      public class Adult :
    IPerson
     
    {
        public string Name { get; set
    ; }
        public int Age { get; set
    ; }
      }

      public interface
    IEmployee
     
    {
        decimal Salary { get; set
    ; }
        void AdjustSalary(decimal
    amount);
      }

      public class Employee : Person,
    IEmployee
     
    {
        public decimal Salary { get; set
    ; }
        public void AdjustSalary(decimal
    amount)
        {
          Salary += amount;
        }
      }

      public class Manager :
    Employee
     
    {
      }
    }

    Now right-click one of the IPerson references, bring up the Jump to... menu and select Interface Implementer. You should see something like the following:

    SelectAnImplementer2

    In general we appear to be in pretty good shape. We've actually written very little code, and yet already we have the ability to list all direct implementers of a particular interface. Also, because of the DXCore's language-independent parse trees, this functionality will work in all languages supported by the DXCore that also support interfaces, including C#, C++, and Visual Basic. This plug-in will also work in languages that we support in the future without a recompile. That's cool.

    So we can list all implementers of an interface, but we're not listing descendants of the direct implementers (consider Employee in the example code above). And of course we still need to add the code to actually move the caret to the selected implementer when the Enter key is pressed. So let's take care of those two things.

    Jumping to Selected Implementers

    Shut down the instance of Visual Studio we're debugging, and return to the CR_JumpToImplementer project.

    First, let's add a property to the FrmPickDescendant form to return the selected node. My first try looks like this:

    public ITypeElement SelectedElement
    {
     
    get
     
    {
        TreeNode
    selectedNode = trvDescendants.SelectedNode;
        if (selectedNode == null
    )
          return null
    ;

        return selectedNode.Tag as ITypeElement
    ;
      }
    }

    Next, let's exploit that property inside our Navigate event handler back in PlugIn1.cs, with the following code:

    private void navJumpToImplementer_Navigate(object sender, DevExpress.CodeRush.Library.NavigationEventArgs ea)
    {
      IElement
    declaration = GetInterfaceDeclaration(ea.Element);
      ITypeElement iTypeElement = declaration as ITypeElement
    ;
      if (iTypeElement != null
    )
      {
        ITypeElement
    [] directImplementers = iTypeElement.GetDescendants();
        if (directImplementers != null && directImplementers.Length > 0
    )
        {
          FrmPickDescendant frmPickDescendant = new FrmPickDescendant
    (directImplementers);
          if (frmPickDescendant.ShowDialog(CodeRush.IDE) == DialogResult
    .OK)
          {
            ITypeElement
    selectedElement = frmPickDescendant.SelectedElement;
            if (selectedElement != null && selectedElement.Files.Count > 0
    )
            {
              CodeRush.File.Activate(selectedElement.Files[0
    ].Name);
              CodeRush.Caret.MoveTo(selectedElement.Ranges[0
    ].Start);
            }
          }
        }
       
    else
          MessageBox.Show(String.Format("There are no implementers of the {0} interface in this solution."
    , declaration.Name));
      }
    }

    ITypeElements have a Files property, accessed here, which contains a list of files where the type is declared. In the case of partial classes, this list might include more than one file. We're using the first file in that list. ITypeElements also have a Ranges property that stores where the type starts and ends inside the source code. That property is also indexed (and this index is synchronized with the index of the Files property). So, for example, the values in Ranges[0] correspond to the file in Files[0]. So the new code above gets the selected element, activates the corresponding file and then moves the caret to the start of that type's declaration.

    Adding Descendants of Direct Implementers

    Let's go back to the FrmPickDescendant form to add the code to list all the implementers of an interface, even those that descend from other classes.

    After a bit of refactoring, I've replaced our form's constructor with the following (the signature is the same -- only the body has changed):

    public FrmPickDescendant(ITypeElement[] directImplementers): this()
    {
      AddImplementers(trvDescendants.Nodes, directImplementers);
    }

    AddImplementers looks like this, and includes a recursive call:

    private void AddImplementers(TreeNodeCollection nodes, ITypeElement[] implementers)
    {
      foreach (ITypeElement iTypeElement in
    implementers)
      {
        TreeNode newTreeNode = new TreeNode
    (iTypeElement.Name);
        newTreeNode.Tag = iTypeElement;
        nodes.Add(newTreeNode);

       
    // Now add descendants...
       
    ITypeElement
    [] descendants = iTypeElement.GetDescendants();
        if (descendants != null && descendants.Length > 0
    )
          AddImplementers(newTreeNode.Nodes, descendants);
      }
    }

    Note that it's theoretically possible to hit an infinite loop here, if a given nodes structure were to arrive in a corrupted state (something that won't compile), for example the case where two classes descend from each other. While this case is unlikely to come up ever, it's a good idea to write some defensive code to stop this. I'll leave that as an exercise for the reader, offering the hint that I would track the full names of each type added, and in the event of duplicates I would exit before recursively calling AddImplementers.

    Testing the Latest Changes

    Great. Now let's run and test again....

    In the test app (if you've been using the sample test code from before), I suggest applying the Move Type to File refactoring for each type, and applying Rename File to Match Type on the last type remaining in Class1.cs. Doing so produces the following files, as seen from Solution Explorer:

    SolutionExplorer

    Now right-click an IPerson reference somewhere in the code and select "Jump to...". Select "Interface Implementer".

    JumpToInterfaceImplementer2

    The Select an Implementer window appears:

    SelectAnImplementer3

    The root nodes of the TreeView are the direct implementers of the interface (Child, Person and Adult all implement IPerson). Now we're also listing descendants of these implementers as child nodes.

    Select an implementer and press Enter. The corresponding file should activate and the caret should move to the start of the class that implements this interface.

    Excellent!

    Now, if you're familiar with existing navigation conventions in CodeRush, you have probably realized by now that this experience is not quite as polished as the CodeRush navigation experience. The recommended guidelines for navigation are essentially as follows:

    1. Drop a Marker (so we can get back easily with Escape)
    2. Activate the file
    3. Move the caret (or the selection) to the desired location
    4. Display a locator beacon if there is no selection (so devs with large monitors can quickly see the new location of the caret)

    So far we have implemented steps 2 and 3. Let's add code for steps 1 and 4.

    Dropping a Marker

    Dropping a Marker at the active caret location is easy. Just call CodeRush.Markers.Drop and pass in the type of the marker desired. In the Navigate event handler inside the PlugIn1.cs file, add the following line of code:

    private void navJumpToImplementer_Navigate(object sender, DevExpress.CodeRush.Library.NavigationEventArgs ea)
    {
      IElement
    declaration = GetInterfaceDeclaration(ea.Element);
      ITypeElement iTypeElement = declaration as ITypeElement
    ;
      if (iTypeElement != null
    )
      {
        ITypeElement
    [] directImplementers = iTypeElement.GetDescendants();
        if (directImplementers != null && directImplementers.Length > 0
    )
        {
          FrmPickDescendant frmPickDescendant = new FrmPickDescendant
    (directImplementers);
          if (frmPickDescendant.ShowDialog(CodeRush.IDE) == DialogResult
    .OK)
          {
            ITypeElement
    selectedElement = frmPickDescendant.SelectedElement;
            if (selectedElement != null && selectedElement.Files.Count > 0
    )
            {

              CodeRush.Markers.Drop(MarkerStyle
    .System);
              CodeRush.File.Activate(selectedElement.Files[0].Name);
              CodeRush.Caret.MoveTo(selectedElement.Ranges[0
    ].Start);
            }
          }
        }
       
    else
         
    MessageBox.Show(String.Format("There are no implementers of the {0} interface in this solution."
    , declaration.Name));
      }
    }

    Displaying a Locator Beacon

    It turns out displaying a locator beacon is pretty easy as well. There's a control, aptly named LocatorBeacon that we can drop on the plug-in....

    Activate the PlugIn1.cs [Design] surface. From the DXCore page of the Toolbox, select the LocatorBeacon control and drop it on the plug-in.

    SelectLocatorBeacon

    Change the LocatorBeacon's Color property if you like. I'm changing it to MediumSeaGreen.

    Next let's add some code to our Navigate event handler to start the beacon animation. The LocatorBeacon has a Start method that accepts a TextView and a line and column position to be the center of the animation. The Navigate method already references the Start property, which holds line and column information, so let's refactor that a bit, introducing a local variable to use for the Start. After these changes my version of the Navigate event handler look like this:

    private void navJumpToImplementer_Navigate(object sender, DevExpress.CodeRush.Library.NavigationEventArgs ea)
    {
     
    IElement
    declaration = GetInterfaceDeclaration(ea.Element);
     
    ITypeElement iTypeElement = declaration as ITypeElement
    ;
     
    if (iTypeElement != null
    )
      {
       
    ITypeElement
    [] directImplementers = iTypeElement.GetDescendants();
       
    if (directImplementers != null && directImplementers.Length > 0
    )
        {
         
    FrmPickDescendant frmPickDescendant = new FrmPickDescendant
    (directImplementers);
         
    if (frmPickDescendant.ShowDialog(CodeRush.IDE) == DialogResult
    .OK)
          {
           
    ITypeElement
    selectedElement = frmPickDescendant.SelectedElement;
           
    if (selectedElement != null && selectedElement.Files.Count > 0
    )
            {
              CodeRush
    .Markers.Drop(MarkerStyle.System);
              CodeRush.File.Activate(selectedElement.Files[0
    ].Name);
              TextPoint start = selectedElement.Ranges[0
    ].Start;
              CodeRush
    .Caret.MoveTo(start);
              locatorBeacon1.Start(CodeRush
    .TextViews.Active, start.Line, start.Offset);
            }
          }
        }
       
    else
         
    MessageBox.Show(String.Format("There are no implementers of the {0} interface in this solution."
    , declaration.Name));
      }
    }

    And that wraps up our changes! There are still some minor UI changes I would like to make to the FrmPickDescendant class, including the following:

    • Smart positioning of the form near the caret
    • Support for double-clicking nodes to accept that node as a target
    • Changing the font size to match the size of the font in the editor
    • Adding more details, like the name of the interface and namespace hints associated with each node
    • Adding a mechanism to filter nodes in the TreeView

    However I'll save those changes for another day. For now I think you have an idea of how easy it is to create a NavigationProvider for CodeRush. Let me know if you have any questions, suggestions, or would like to see more step-by-step tutorials like this in the future.

  • CodeRush Shortcuts and Templates - Quick Start Sheets

    CodeRush shortcuts and templates quick start sheets are now available in PDF format. Click the image below to get them.

    CodeRush Shortcuts

  • Announcing CodeRush Xpress for C#

    Developer Express and Microsoft are proud to announce a new version of CodeRush licensed exclusively for C# developers working in Visual Studio. The new product is called CodeRush Xpress, and it includes a fresh selection of hand-picked features taken from CodeRush and Refactor! Pro.

    Here's a sampling of what you get:

    Find any File or Symbol...

    Go to any file or symbol in the solution efficiently.

    Quick File Navigation

    Type in a few letters of the name to filter...

    Quick File Navigation-call

    Or hold down the shift key to filter only on uppercase letters...

    Quick File Navigation-R

    Uppercase characters can appear anywhere. Any uppercase characters not specified in the filter are highlighted in blue...

    Quick File Navigation-RC

    The uppercase letters need not be sequential. Notice how "RD" in the screen shot below matches both "ResolveDelegate.cs" and "ResolveCallbackDelegate.cs"

    Quick File Navigation-RD  

    Tab to Next Reference

    This is a really cool navigation feature that takes you to the next (and previous) reference just by pressing the Tab key (or Shift+Tab key to go backwards) when the caret is inside an identifier, member, or a type. For example, in the following code, the caret was inside the last Person type reference (in the "new Person()" instantiation call) when the Tab key was pressed, causing the Person reference at the top of the file to become highlighted.

    TabToNextReference

    Expand/Shrink Selection

    This feature allows you to increase a selection by logical blocks, or decrease an expanded selection by the same logical blocks. This feature is perfect when refactoring, since many refactorings work on a contiguous selection of code (e.g., extract method, introduce local, etc.).

    TDD-Style Intelligent Declaration Based on Usage

    Place the caret on any undeclared element in your code and press the Refactor/Code key (Ctrl+`) to see list of intelligent suggestions for the type of the new variable. For example, in the screen shot below two appropriate types are suggested, even though Console.WriteLine has many overloads that accept a wide variety of types.

    DeclareLocalTwoOptions

    You can even turn a function call or property access into a variable declaration.

    DeclareLocalOnProperty

    This makes writing code with Visual Studio's Intellisense really fast. Just use Intellisense to produce the property reference or function call, and then press the Refactor/Code key to declare a new local variable of the appropriate type. Here's an example with a function call -- notice the caret can be just about anywhere on the function call, even at the end of the line.

    DeclareLocalOnFunction

    You can even declare a local from a simple instantiation, like this:

    DeclareLocalNewPerson

    And it's worth noting you can declare all kinds of elements based on usage, not just locals. Anything you need types, members, fields -- the full list of elements that can be declared appears below.

    Professional Grade Refactorings

    CodeRush Xpress includes many powerful refactorings to help improve the quality of your code. For example, consider the following:

        private static void ShowInt(int n)
        {
         
    Console.WriteLine(n);
        }
       
    private static void ShowEntries(List<int> entries)
        {
          entries.ForEach((
    Action<int>)ShowInt);
        }

    With the caret on the ShowInt method reference, you can press the Refactor! key...

    InlineDelegate 

    and then select Inline Delegate. This will produce the following code:

        private static void ShowEntries(List<int> entries)
        {
          entries.ForEach(
    delegate(int n)
                          {
                           
    Console.WriteLine(n);
                          });
        }

    Notice the caret is on the delegate keyword. You can immediately press the Refactor! key again...

    CompressToLambdaExpression

    Note that we can go in two directions now. We can either convert the anonymous method to a named method (in essence reversing the Inline Delegate refactoring we just performed), or we can take it a step further and compress the anonymous method into a lambda expression. Choosing Compress to Lambda Expression, we get the following:

        private static void ShowEntries(List<int> entries)
        {
          entries.ForEach(n =>
    Console.WriteLine(n));
        }

    CodeRush Xpress is loaded with powerful refactorings taken from Refactor! Pro. One of our favorites, Extract Method to Type, allows you to extract a method from one type into another, updating both the calling code and the extracted method appropriately. For an example, consider the following code:

      class Person
      {
        public string Name { get; set; }
        public bool IsAnOrphan { get; set; }
        public Person Mother { get; set; }
        public Person Father { get; set; }
      }
    // ...
      class BabyMaker
      {
        public Person MakeOne(Person mother, Person father, string name)
        {
          Person newBaby = new Person();
          newBaby.Name = name;
          newBaby.Mother = mother;
          newBaby.Father = father;
          newBaby.IsAnOrphan = newBaby.Mother == null && newBaby.Father == null;
          return newBaby;
        }
      }

    Notice that the code in the MakeOne method contains a local variable that's an instance of type Person, and that code sets the IsAnOrphan property. There is some logic in this method that pertains to Person, but it feels like it's in the wrong class!

    We already have the class Person in our solution, so it makes sense to move some of this code to the proper class. With CoderRush Xpress installed, all we need to do is select the code we want to move....

    SelectCodeToExtract 

    Press the Refactor key...

     ExtractMethodToPerson3

    And choose "Extract Method to Person". CodeRush Xpress analyzes the selection and determines there's at least one local variable of a type you have declared elsewhere in the solution. Now let's apply this refactoring...

     TargetPicker2

    Press the Up and/or Down arrow keys to select a location for this new method, and press Enter to commit. Give the method a meaningful name...

    SetParents

    Cool. Now we have the logic (that should have been inside Person to start with) right where it belongs. Notice how the new SetParents instance method works on the instance itself (compare that code with the original code that operated on the newBaby local). The code is easier to read and cleaner in both locations.

    Notice also that tiny dark blue triangle, in the code above. That's a stack-based marker, and CodeRush Xpress drops markers automatically whenever you apply a refactoring or TDD-style declaration that takes you away from the original location. You can jump back at any time to the top marker on the stack by pressing Escape.

    There are many more refactorings and cool features in CodeRush Xpress. This is just a preview. Here's the full list of what you get:

    Editor Features

    • Duplicate Line
    • Highlight Usages
    • Clipboard Features
      • Smart Cut/Copy
      • Paste Replace
    • Enhanced Selection Abilities
      • Extend/reduce selection
      • Camel-case selection

    Navigation Features

    • Camel-case Navigation
    • Tab to Next Reference
    • Go to File
    • Go to Symbol (QuickNav)

    TDD - Declaration from Usage

    • Types
      • Declare Class
      • Declare Delegate
      • Declare Enum
      • Declare Enum Element
      • Declare Interface
      • Declare Struct
    • Members
      • Declare Constructor
      • Declare Event Handler
      • Declare Getter
      • Declare Method
      • Declare Property
      • Declare Property (auto-implemented)
      • Declare Property (with backing field)
      • Declare Setter
    • Variables
      • Declare Field
      • Declare Local
      • Declare Local (implicit)

    Refactorings

    • Add/Remove Block Delimiters
    • Combine Conditionals (merge nested "If" statements)
    • Compress to Lambda Expression
    • Compress to Ternary Expression
    • Convert to Auto-implemented Property
    • Convert to Initializer (use object/collection initialize when possible)
    • Create Backing Store (converts Auto-implemented Property to standard Property with get and set)
    • Decompose Initializer
    • Decompose Parameter
    • Expand Lambda Expression
    • Expand Ternary Expression
    • Extract Method to Type
    • Flatten Conditional
    • Introduce Local (introduce variable)
    • Inline Delegate
    • Inline Temp (inline variable)
    • Make Explicit
    • Make Implicit
    • Move Type to File
    • Name Anonymous Method
    • Name Anonymous Type
    • Reverse Conditional (invert "if")
    • Split Conditional (split complex "If" statements)
    • Use StringBuilder
    • Use String.Format
  • If there's one thing you see at PDC...

    If there's one thing you see, one place you want to be... this is it.

    Time: Monday night, 6:55pm

    Place: PDC. Exhibition hall. The Oversized 30x30 DevExpress booth.

    At this time and place, DevExpress will be showing quite likely the most amazing thing ever presented at any technical conference ever (if not the most amazing thing anyone has ever seen or will ever see in the entire history and future of humanity). And that's an understatement. I haven't even begun to raise expectations yet.

    I'm not kidding.

    This will be the event that everyone will be blogging about and talking about. If you're there, you'll be among those who will speak with authority. If you're not, well, I don't even want to look at your face anymore.

    Seriously.

    Now, back to work on cloning my new assistant for the demo....

  • Think I'm Starting a Trend

    Looks like Julian has started to fancy my new Mr. Clean look.

  • IDE Team Discussion - Using CodeRush Templates to Generate Code

    CodeRush users interested in creating templates that generate custom code based on elements inside a container (e.g., fields in a class, methods in a type, types in a namespace, comments in a file, etc.), might want to check out this YouTube video.

    In it, the IDE Team discusses some work we're doing, where we need to add custom code to serialize and deserialize the fields of around 30 classes. The solution came in creating a template that iterates through the fields in each class and generates the appropriate serialization or deserialization code for each field.

    The main template looks like this:

    «:ccsr»

    public override void WriteData(BinaryWriter writer)
    {
     base.WriteData(writer);
     «ForEach(Field in this, WriteField)»}
     
    public override void ReadData(BinaryReader reader)
    {
     base.ReadData(reader);
     «ForEach(Field in this, ReadField)»}

    Both the ReadField and WriteField templates will be called once for each field in the active class. Both of these templates have a number of alternate expansions. The expansion ultimately selected for a particular field is determined by context. You can set context with the Context Picker on the lower right of the Template options page.

    To make this work, we created a new context, called TypeImplements, because many of the scenarios we needed to respond to were dependent upon the type of the field. For example, one of the alternate expansions for ReadField has this context:

    TypeImplements(«?Get(itemType)»,System.Boolean)

    You can pass parameters to contexts (like we've done here), by right-clicking the context in the Context Picker, and selecting "Parameters...".

    The expansion for the ReadField template associated with the context above looks like this:

    «?Get(itemName)» = reader.ReadBoolean();

    «?Get(itemName)» returns the name of the field we're iterating over, while «?Get(itemType)» returns the full type name. Get is a custom StringProvider that you can use to retrieve the value of a template variable stored with the Set StringProvider (the ForEach TextCommand stores the itemName and itemType variables for you automatically before calling the ReadField and WriteField templates).

    The new TypeImplements context added to solve this code generation challenge will ship with the next version of CodeRush.

  • Language Translation on Paste with the Clipboard History Plug-in

    Welcome to Part 12 of the series showing how to build plug-ins for Visual Studio using DXCore.

    So far we've:

    Today, in our final post of the 12-part series, we'll add a neat feature that shows how to paste code copied from one language into another.

    Paste As Language Conversions

    I know a number of developers who work in more than one language. And sometimes when researching a question online I'll find example code in the wrong language. While the DXCore was never specifically built to convert from one language to another, it has a pretty decent (but not always 100% perfect) ability to do so. And so I'm thinking it might be nice to offer the ability to convert code copied from one language into another just before pasting.

    I also like this feature because it shows how to convert source code from one language to another using the DXCore.

    So let's add a "Paste as..." menu item which can convert code in the clipboard history into the active language. This menu item would be available when the active language is different from the language of the entry.

    Right-click the horizontal bar in our ContextMenuStrip and choose Insert | MenuItem.

    InsertMenuItem

    Set these properties:

    Property Value
    (Name) itmPasteAs
    Text Paste as

    PasteAsMenuItem

    Create an event handler for this menu item's Click event....

    itmPasteAsClickEventHandler

    In the handler, add a call to PasteAsTargetLanguage (we'll implement this soon):

    private void itmPasteAs_Click(object sender, EventArgs e)
    {
      PasteAsTargetLanguage();
    }

    Next, let's ensure this menu item is only available under the right conditions, e.g., when the selected clipboard entry contains source code in a language that is different from the language of the active source file.

    In the FrmClipHistory.cs [Design] file, click the contextMenuStrip1 control.

    SelectContextMenu2

    In the Properties grid, click the Events tab.

    contextMenuStripClickEvents

    Create a handler for the Opening event. This event fires just before the context menu appears.

    contextMenuStripOpeningEventHandler

    Inside the Opening handler, add the following code:

    private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
    {
      itmPasteAs.Visible = false;
      LanguageExtensionBase sourceLanguage;
      LanguageExtensionBase targetLanguage;
      GetLanguagesForConversion(out sourceLanguage, out targetLanguage);
      if (sourceLanguage == null || targetLanguage == null)
        return;
      itmPasteAs.Visible = true;
      itmPasteAs.Text = "Paste as " + targetLanguage.LanguageID;
    }

    This code will make the itmPasteAs menu item visible only if the language from which the source code in the current entry was copied.... LEFT OFF HERE

    GetLanguagesForConversion determines the source and target languages necessary for conversion, and looks like this:

    private static void GetLanguagesForConversion(out LanguageExtensionBase sourceLanguage, out LanguageExtensionBase targetLanguage)
    {
      sourceLanguage = null;
      targetLanguage = null;
      ClipboardHistoryEntry entry = ClipboardHistory.GetEntry(CursorIndex);
      if (entry == null || String.IsNullOrEmpty(entry.Language) || entry.Language == CodeRush.Language.Active)
        return;
      sourceLanguage = CodeRush.Language.GetLanguageExtension(entry.Language);
      targetLanguage = CodeRush.Language.ActiveExtension;
     
    // Only return languages that supports types....
     
    if (sourceLanguage != null && !sourceLanguage.SupportsTypes)
        sourceLanguage = null;
      if (targetLanguage != null && !targetLanguage.SupportsTypes)
        targetLanguage = null;
    }

    Notice the check in the code above to make sure both languages return true in their SupportsTypes properties. This check is performed because there are languages supported by the DXCore that do not support types, such as HTML, and converting from HTML to Visual Basic, for example, would probably not yield useful results. So this simple check should ensure that both the language of the clipboard entry and the language of the active file we're about to paste into, both have a good chance of producing a relatively useful conversion.

    Finally, our PasteAsTargetLanguage method looks like this:

    private void PasteAsTargetLanguage()
    {
      LanguageExtensionBase sourceLanguage;

      LanguageExtensionBase
    targetLanguage;
      GetLanguagesForConversion(out sourceLanguage, out targetLanguage);
      if (sourceLanguage != null && targetLanguage != null)
      {
        ParserBase parser = CodeRush.Language.GetParserFromLanguageID(sourceLanguage.LanguageID);
        if (parser != null)
        {
          LanguageElement rootNode = parser.ParseString(ClipboardHistory.GetText(CR_ClipboardHistory.FrmClipHistory.CursorIndex));
          if (rootNode != null)
          {
            CloseAndPaste(CodeRush.Language.GenerateElement(rootNode, targetLanguage));
            return;
          }
        }
      }
      CloseAndPaste();
    }

    This method gets the source languages necessary for conversion, and then gets a parser for the source language, calling ParseString to turn that code into a parse tree. Then it takes that parse tree and feeds it to GenerateElement to get the source code equivalent in the target language. With the conversion complete all that's needed is to pass those results on to CloseAndPaste. And speaking of CloseAndPaste, I needed to refactor that method so it could optionally accept the text to paste (instead of taking it directly from the ClipboardHistory). The refactored CloseAndPaste and its corresponding overload (to prevent existing code from breaking) look like this:

    private void CloseAndPaste()
    {
      string thisText = ClipboardHistory.GetText(CursorIndex);
      CloseAndPaste(thisText);
    }

    private void CloseAndPaste(string textToPaste)
    {
      Application.RemoveMessageFilter(this);
      PasteOnClose = true;
      if (textToPaste != Clipboard.GetText() && !String.IsNullOrEmpty(textToPaste))
    // Need to put text on clipboard.
       
    Clipboard.SetText(textToPaste);
     
    // Simply calling close here will fail when a mouse double-clicks on a CodeView.
     
    NativeMethods.PostMessage(this.Handle, WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    }

    Adding this optional parameter makes it possible for us to condition the text before pasting (such as what we're doing here in converting code from one language to another).

    Finally, let's add keyboard support for this so developers can paste translated source code without being forced to reach for the mouse. We've already assigned the Enter key to our Paste functionality, so it seems logical to make Alt+Enter be our alternate "Paste as..." shortcut. This requires a small addition to our ProcessCmdKey method:

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
     
    const int WM_KEYDOWN = 0x100;
     
    if (msg.Msg == WM_KEYDOWN)
     
    {
       
    switch (keyData)
        {
         
    case Keys.Enter:
           
    CloseAndPaste();
           
    return true;

          case Keys.Enter | Keys.Alt:
            PasteAsTargetLanguage();
            return true;

         
    case Keys.Escape:
           
    Close();
           
    return true;
       
    }
       
    switch (keyData & ~Keys.Control)
       
    {
         
    case Keys.Left:
           
    MoveCursor(0, -1);
           
    return true;

         
    case Keys.Right:
           
    MoveCursor(0, 1);
           
    return true;

         
    case Keys.Up:
           
    MoveCursor(-1, 0);
           
    return true;

         
    case Keys.Down:
           
    MoveCursor(1, 0);
           
    return true;
       
    }
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

    And that's it for the code changes. One thing I've noticed, however, is when we bring up the context menu on the Clipboard History, there are no shortcuts shown in the menu. For example, Enter is already our Paste command trigger, however you see no mention of that when the menu is up:

    NoShortcuts

    So let's fix this. I noticed that the ShortcutKeys property of the ToolStripMenuItem control seems to be missing an option to compose a shortcut with the Enter key. Fortunately this control also has a string property ShortcutKeyDisplayString, which will work.

    Set the ShortcutKeyDisplayString properties of the two Paste and Paste as menu items to "Enter" and "Alt+Enter", respectively.

     ShortcutKeyDisplayStringAltEnter

    The form should now look like this:

     ShortcutKeysOnContextMenuAltEnter

    Now it's time to test. Click Run. Open a file in one language (e.g., C#), and copy some code to the clipboard. For example, I've selected this method with a C# initializer in it:

    private static Form CreateNewWindow()
    {
      var window = new Form { Name = "New window", Width = 640, Height = 480 };
      return window;
    }

    Open a file in a different language (e.g., VB), press Ctrl+Shift+Insert to bring up the Clipboard History, right click the entry to paste, and select "Paste as Basic" (or whatever language you've selected.

     PasteAsBasic3

    Here's the code I get when pasting in Visual Basic:

    Private Shared Function CreateNewWindow() As Form
      Dim window = New Form() With {.Name = "New window", .Width = 640, .Height = 480}
      Return window
    End Function

    Excellent! As I alluded to at the beginning of this post, language conversion has never been the number one priority with the DXCore. It's really more a side-effect of the architecture that you get for free, so you may find examples where the results require some editing (especially in areas where features exist in the source language but not the target language). So it's not always perfect, but it's still pretty cool.

    Wrap Up

    For now I think this Clipboard History feature is done. We've come a long way since part one of this series, where we had an idea for a simple plug-in to provide quick access to a history of clipboard operations. Now we have syntax highlighting, ease of use, discoverability, persistence, customization (with preview) and language conversion, all in a professional package with concise code that should be easy to maintain. And this series should have given you a sense of what it takes to produce a high quality feature that many developers will enjoy.

    Speaking of which, CodeRush customers will get this feature (and full source code) wrapped up into the next release (3.2). For eveyone else, you'll need to download the DXCore and follow the steps of this series (or you could purchase a license CodeRush -- it rocks on multiple levels)....

    So what's next? Well, the next step is up to you. What cool feature will you build and share with your peers? Let us know what you're working on and what we can do to make writing plug-ins even easier and more enjoyable. Also, let us know what parts of the DXCore we should cover in more depth with tutorials like this.

  • Improving Discoverability with our Clipboard History DXCore Plug-in for Visual Studio

    Welcome to Part 11 of the series showing how to build plug-ins for Visual Studio using DXCore.

    So far we've:

    In today's post, we'll improve shortcut discoverability and add a context menu to our Clipboard History. We'll also learn how to programmatically invoke the options page we created earlier, and see yet another way to work with DecoupledStorage.

    Improving Shortcut Key Discoverability

    So far the shortcut and visual interaction feel good, but discoverability for the shortcut keys could be better. To make shortcuts easier to discover, I'm thinking about placing a status bar below the views, revealing available keys.

    So let's try that. From the Toolbox, select the StatusStrip control.

    SelectStatusStrip

    Drop the StatusStrip onto our FrmClipHistory form...

     DropStatusStrip

    Click the Add drop down button.

    ClickAddLabel 

    Select the StatusLabel.

    AddStatusLabel

    Set the following properties for the new StatusLabel:

    Property Value
    (Name) lblStatusText
    Enabled Arrow keys to select, Enter to paste, Escape to cancel. Hold down Ctrl to maximize code view.

    Next, let's update that AllViewsRect property to take this new status bar into consideration:

    private static Rectangle AllViewsRect
    {
     
    get
     
    {
        if (_FrmClipHistory == null)
          return Rectangle.Empty;
        Rectangle rect = _FrmClipHistory.ClientRectangle;
        if (_FrmClipHistory.statusStrip1.Visible)
          rect.Height -= _FrmClipHistory.statusStrip1.Height;
        return rect;
      }
    }

    That should be all we need. Run and test to make sure that it's working as expected.

    Now that we have a status bar, it might be nice to allow developers familiar with the shortcuts to hide the status bar to regain some screen real estate for clipboard entries. It would also be nice to provide a fast and easy way to get directly to our new options page from the Clipboard History UI. Both of these problems can be solved with a right-click context menu....

    Adding a Context Menu

    Let's add a context menu with items to:

    • Paste the selected entry
    • Make it easy to discover and access our options page
    • Toggle the visibility of the status bar

    If you're still running a debugger session, close it now.

    Activate FrmClipHistory.cs [Design].

    ActivateFrmClipHistoryDesign

    From the Toolbox, select a ContextMenuStrip control...

    SelectContextMenu

    Drop the ContextMenuStrip onto the form...

    DropContextMenuStrip

    Using the in-place editor...

    AddNewOptionsMenuItem

    Add four new menu items:  "Paste", "-", "Toggle status bar visibility", and "Options...".

    AddMenuItems 

    Let's create event handlers for each menu item. First, double-click on the Paste menu item, and inside that event handler add a call to CloseAndPaste, like this:

    private void pasteToolStripMenuItem_Click(object sender, EventArgs e)
    {
      CloseAndPaste();
    }

    Activate the FrmClipHistory.cs [Design] form again, and then double-click the "Toggle status bar visibility" menu item. Add the following code to that event handler:

    private void toggleStatusBarVisibilityToolStripMenuItem_Click(object sender, EventArgs e)
    {
      statusStrip1.Visible = !statusStrip1.Visible;
      PositionViews();
      using (DecoupledStorage storage = OptClipboardHistory.Storage)
        storage.WriteBoolean("Settings", "ShowStatusBar", statusStrip1.Visible);
    }

    I think it's important to save this preference out whenever it changes. That way developers will only have to hide it once.

    Using DecoupledStorage

    One of the cool things about this plug-in is that it uses DecoupledStorage in three different but interesting ways:

    • To save and load the clipboard history.
    • To save and load options page settings.
    • To save configuration settings that are not on any options page.

    This last bullet point is what we're doing now. While we could place a new check box on our options page, having an option to show or hide the status bar is pretty trivial, and may introduce some confusion. Instead, we can provide contextual access to the setting through the right-click menu. This also has the benefit of giving developers immediate feedback on the setting (to get the same effect with a check box on an options page we would have to modify the preview to draw a status bar -- again, more work than seems justified).

    Next, let's make sure the status bar is hidden if needed on startup. First, add a call to LoadSettings from inside ShowClipboardHistory:

    public static void ShowClipboardHistory()
    {
      if (_FrmClipHistory != null)
       
    return;
     
    _FrmClipHistory = new FrmClipHistory();
     
    PasteOnClose = false;
     
    try
     
    {
        Application.AddMessageFilter(_FrmClipHistory);
     

        LoadSettings(); 

       
    CreateViews();
       
    PositionViews();
       
    UpdateViews();
       
    ShowCursor();
       
    PositionForm();
       
    _FrmClipHistory.ShowDialog(CodeRush.IDE);
     
    }
     
    finally
     
    {
       
    CleanUpViews();
       
    Application.RemoveMessageFilter(_FrmClipHistory);
       
    if (_FrmClipHistory != null)
         
    _FrmClipHistory.Dispose();
       
    _FrmClipHistory = null;
     
    }
     
    if (PasteOnClose)
       
    Paste();
    }

    It is important to call LoadSettings before calling PositionViews, since CodeView position depends upon that AllViewsRect property, which changes based on status bar visibility.

    LoadSettings looks like this:

    private static void LoadSettings()
    {
      using (DecoupledStorage storage = OptClipboardHistory.Storage)
        _FrmClipHistory.statusStrip1.Visible = storage.ReadBoolean("Settings", "ShowStatusBar", true);
    }

    Activate the FrmClipHistory.cs [Design] form again, and then double-click the "Options..." menu item. Add the following code to that event handler:

    private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
    {
      OptClipboardHistory.Show();
    }

    Now I think this is really cool. Take a look at that code in the event handler. It's calling a static method inside our options page. That method was built courtesy of the wizard, and it brings up the DevExpress options dialog and displays this page.

    One last important step: We need to assign our new context menu to the form's ContextMenuStrip property, like this:

    AssignContextMenuStripProperty

    ...

    ContextMenuStripAssigned

    Nice. Let's give this a try.

    Testing the Context Menu

    Click Run. In the second instance of Visual Studio, press Ctrl+Shift+Insert to bring up the Clipboard History form. Right-click the form to bring up the context menu. Try the Paste and Toggle status bar visibility menu items to verify expected behavior. Try hiding the status bar, closing the form, and then bringing it up again to verify that the status bar visibility setting is in fact preserved.

    Not bad. Although I am noticing that when I right-click, the CodeView under the mouse is not selected, and I think it should be.

    We can fix that while we're running if edit and continue is enabled. Just set a breakpoint in the PreFilterMessage method, and when it hits make this change (add the check for the right mouse button):

    bool IMessageFilter.PreFilterMessage(ref Message m)
    {
      bool isDoubleClick;
      if (m.Msg == (int)WindowMessage.WM_LBUTTONDOWN
    || m.Msg == (int)WindowMessage.WM_RBUTTONDOWN
    )
        isDoubleClick = false;
      else if (m.Msg == (int)WindowMessage.WM_LBUTTONDBLCLK)
        isDoubleClick = true;
      else
    // Not a message we're interested in.
        return false;
     
      Control target = Control.FromHandle(m.HWnd);
      if (target is WheelPanel)
    // WheelPanel is the child of the CodeView that holds the text.
        target = target.Parent;

      if (target != null)
        for (int i = 0; i <= ClipboardHistory.LastIndex; i++)
          if (_Borders[ i ] == target || _CodeViews[ i ] == target)
          {
            MoveCursorTo(i);
            if (isDoubleClick)
              CloseAndPaste();
            return true;
          }
      return false;
    }

    And then clear out the breakpoint and run again. Now you should be able to right-click any CodeView, and have it become selected before the context menu pops up.

    Next, let's test bringing up the options page. Right-click the Clipboard History form, and choose "Options...".

    InvokeOptions

    Remember this line of code?

      OptClipboardHistory.Show();

    That static method is all it takes to bring up the options dialog and have our new options page displayed.

    With the Options dialog up, change only the Selector Color (don't touch the dimensions just yet)...

    ChangeOnlyColor

    and click OK.

    ClickOKOptionsDialog

    And yet the cursor color does not appear to have changed:

    CursorColorNotChanged

    Press one of the arrow keys to move the cursor, and you'll see the cursor color correctly drawn.

    CursorColorChanged

    OK, so it seems we need to call ShowCursor on the form after returning from the options page. I'm a big fan of efficient code, so let's only call ShowCursor if the color changes, by adding the following code to our Options... menu item event handler:

    private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
    {
      Color previousColor = SelectedBorderColor;

     
    OptClipboardHistory.Show();

      if (previousColor != SelectedBorderColor)
        ShowCursor();
    }

    You can add this code by setting a breakpoint at the opening brace of the handler (bringing up the options page again through the context menu) and using Edit and Continue, or you can shut down the debugging session, make the change, and then start a new debugging session (Edit and Continue is much faster).

    With the change made, let's repeat the test again. Right-click the Clipboard History, select "Options...", and then change only the Selector Color (we'll test dimension changes in a bit), and then click OK.

    CursorColorChangedImmediately

    This time the cursor color is updated immediately when the options page closes.

    Excellent.

    Now, let's try changing the dimensions. Specifically, try increasing one or both of the dimensions. I've been holding off testing a dimension change because I'm pretty sure we're going to see some issues. Much of the code in FrmClipHistory depends upon a certain synchronization between the values in ClipboardHistory (e.g., RowCount, ColumnCount), and the CodeViews and border Panels on the form.

    So, after right-clicking the context menu and bringing up the Options dialog one more time, increasing the dimensions and clicking OK, we might see something like this:

    NotGood

    Not only is there no indication that our change in dimensions has taken place, but there also appears to be two cursors! Interacting further with the Clipboard History and you might find yourself suddenly staring at a dialog back in the first instance of Visual Studio, looking something like this:

    OutOfRange

    And if you click Continue, you might eventually see something like this:

    VisualStudioIsClosing

    So clearly we have at least one issue that requires attention.

    One solution might be to close the Clipboard History and reopen it again after returning from the call to OptClipboardHistory.Show. However I think we can clean things up and rebuild the dialog while it's still up, without closing the form.

    We should be able to fix this by adding the following code to our Options... click event handler:

    private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
    {
     
    Color previousColor = SelectedBorderColor;

      int previousRowCount = ClipboardHistory.RowCount;
      int previousColumnCount = ClipboardHistory.ColumnCount;

     
    OptClipboardHistory.Show();

     
    if (previousRowCount != ClipboardHistory.RowCount || previousColumnCount != ClipboardHistory.ColumnCount)
      {
        int previousCount = previousRowCount * previousColumnCount;
        for (int i = 0; i < previousCount; i++)
        {
          Controls.Remove(_Borders[ i ]);
          _CodeViews[ i ] = null;
          _Borders[ i ] = null;
        }
        CreateViews();
        PositionViews();
        UpdateViews();
        ShowCursor();
      }
      else
    if (previousColor != SelectedBorderColor)
       
    ShowCursor();
    }

    I put the else in front of the last if-statement (that checks for color change) for efficiency, as there is already a ShowCursor call in the previous block (there's no need to call this twice if the dimensions change).

    Now, let's try to test this again.

    Click Run. Press Ctrl+Shift+Insert to bring up the Clipboard History.

    Right-click the Clipboard History and choose "Options...".

    Change the dimensions of the clipboard history and click OK. Try changing dimensions again. Be sure to test changes where you increase the dimensions.

    This time changes to row or column count appear to be reflected immediately in the Clipboard History upon closing the options dialog.

    Not bad, but in testing you've probably noticed that this new code appears to have introduced a very strange bug. After clicking OK on the options dialog and returning to the Clipboard History, the arrow keys no longer move the cursor! If I switch focus to another application and then return to this instance of Visual Studio, the arrow keys work normally again as expected.

    What's going on?

    If I set a breakpoint inside the ProcessCmdKey method, and then repeat the steps to reproduce the bug (switching to the first instance of Visual Studio to set the breakpoint fixes the problem when I switch back -- so that's why I need to repeat the steps), I find that the ProcessCmdKey breakpoint is NEVER HIT when the problem is reproduced.

    This is a very strange problem indeed.

    After playing with this more, I theorized that the form is somehow no longer considering itself active. It doesn't make sense to me why this would happen, however the behavior certainly seems to indicate that.

    My first stab at fixing this was to place a call to Activate at the end of the block that rebuilds and repositions the CodeViews (right after the first ShowCursor call).

    Unfortunately this call to Activate had no effect on the problem. Changing the dimensions through the right-click context menu still resulted in completely disabled keyboard functionality.

    Then I tried adding a call to Focus instead, and remarkably, THAT WORKED.

    Here's the final version of the event handler, with that added call to Focus that restores keyboard functionality:

    private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
    {
     
    Color previousColor = SelectedBorderColor;
      int previousRowCount = ClipboardHistory.RowCount;
     
    int previousColumnCount = ClipboardHistory.ColumnCount;
     
    OptClipboardHistory.Show();
     
    if (previousRowCount != ClipboardHistory.RowCount || previousColumnCount != ClipboardHistory.ColumnCount)
     
    {
       
    int previousCount = previousRowCount * previousColumnCount;
       
    for (int i = 0; i < previousCount; i++)
       
    {
         
    Controls.Remove(_Borders[ i ]);
         
    _CodeViews[ i ] = null;
         
    _Borders[ i ] = null;
       
    }
       
    CreateViews();
       
    PositionViews();
       
    UpdateViews();
       
    ShowCursor();

        Focus();

      }
     
    else if (previousColor != SelectedBorderColor)
       
    ShowCursor();
    }

    I suspect a few minutes spent with Reflector may reveal what's happening here.

    Test this new code and see what you think.

    Tomorrow, we'll add one of the last features on my list, the ability to paste in a different language (e.g., copy from a VB file and paste into a C# file). See you then!

  • Maximizing the Active CodeView in our Clipboard History Plug-in for Visual Studio

    Welcome to Part 10 of the series showing how to build plug-ins for Visual Studio using DXCore.

    So far we've:

    In today's post, we'll add the ability to quickly maximize and later restore the active CodeView in the Clipboard History.

    Maximizing the Active CodeView

    I want to improve the Clipboard History UI a bit. While we have increased flexibility by allowing developers the ability to change the dimensions of the Clipboard History, having many rows and/or columns may lead to views that clip a portion of their content, forcing developers to scroll to see everything they contain.

    So what I would like to do is make it possible to maximize the active CodeView temporarily, by holding down the Ctrl key. This allows developers working in space-constrained environments the ability to quickly see the contents of a selected clipboard history entry, without having to reach for the mouse to scroll.

    Unfortunately I'm not aware of any elegant code which can notify me the moment the Ctrl key is pressed or released, so I'm going to hack this the old-fashioned way: by polling with a Timer (shudder).

    Activate FrmClipHistory.cs [Design].

    ActivateFrmClipHistoryDesign

    From the Toolbox, select a System.Windows.Forms.Timer control...

    SelectTimer

    Drop the Timer onto the FrmClipHistory design surface...

    DropTimer

    Set the following properties:

    Property Value
    (Name) tmrCheckControl
    Enabled True

    In the Properties grid, click the Events button.

    tmrCheckControlClickEventsButton

    Double-click the Tick event to add a new handler.

    HandleTickEvent

    Add the following code...

    private static bool _ActiveViewMaximized;

    private
    void tmrCheckControl_Tick(object sender, EventArgs e)
    {
      Keys controlState = Control.ModifierKeys & Keys.Control;
      if (!_ActiveViewMaximized && controlState == Keys.Control ||
          _ActiveViewMaximized && controlState != Keys.Control)
      {
        _ActiveViewMaximized = !_ActiveViewMaximized;
        PositionViews();
      }
    }

    And let's now modify PositionViews so it can distribute the CodeViews evenly or maximize the view under the cursor. Speaking of PositionViews, this method is getting a little meaty, and with this new code it's going to get even meatier, so I think we should refactor it a bit. Here's the new PositionViews method:

    private static void PositionViews()
    {
      if (_FrmClipHistory == null)
        return;

      if (_ActiveViewMaximized)
        MaximizeActiveView();
      else 
        DistributeViewsEvenly();
    }

    DistributeViewsEvenly contains essentially the code from the old PositionViews method:

    private static void DistributeViewsEvenly()
    {
      int width = AllViewsRect.Width / ClipboardHistory.ColumnCount;
      int height = AllViewsRect.Height / ClipboardHistory.RowCount;
      Size viewSize = new Size(width - CursorBorder * 2, height - CursorBorder * 2);
      Size borderSize = new Size(width, height);
      for (int row = 0; row < ClipboardHistory.RowCount; row++)
        for (int column = 0; column < ClipboardHistory.ColumnCount; column++)
        {
          int index = GetIndex(row, column);
          CodeView thisView = _CodeViews[index];
          Panel thisBorder = _Borders[index];
          if (thisView == null || thisBorder == null)
            continue;

          thisBorder.Size = borderSize;
          thisBorder.Location = new Point(width * column, height * row);
          thisView.Size = viewSize;
        }
    }

    I replaced access to _FrmClipHistory.ClientRectangle from inside that old PositionViews code with an AllViewsRect property access. The AllViewsRect property simply returns the client rectangle of the form, like this:

    private static Rectangle AllViewsRect
    {
      get { return _FrmClipHistory.ClientRectangle; }
    }

    MaximizeActiveView needs this rectangle as well, and now seems like a good time to consolidate this access anyway, because I'm thinking about adding a status bar soon to assist with discoverability. If we do this, that status bar will reduce available space to be something less than ClientRectangle. Later, if/when we need to calculate the new available space (e.g., due to this new status bar I'm thinking about adding to the form), we can make that calculation from a single location.

    MaximizeActiveView looks like this:

    private static void MaximizeActiveView()
    {
      int cursorIndex = GetIndex(_CursorRow, _CursorColumn);
      CodeView cursorView = _CodeViews[cursorIndex];
      Panel cursorBorder = _Borders[cursorIndex];
      if (cursorView == null || cursorBorder == null)
        return;

      Rectangle viewRect = AllViewsRect;
      viewRect.Inflate(-CursorBorder, -CursorBorder);
      cursorView.Size = viewRect.Size;
      cursorBorder.Size = AllViewsRect.Size;
      cursorBorder.Location = new Point(0, 0);
      cursorBorder.BringToFront();
    }

    That last BringToFront call is important -- it allows us to only size and position the active view and hide all the others behind it (regardless of their positions).

    There's one more change required. If the active view is maximized, and the arrow keys are pressed, I still want the cursor to move and the active view to reflect that. To do this, we'll need to add the following code to MoveCursor:

    private static void MoveCursor(int rowDelta, int columnDelta)
    {
     
    HideCursor();
     
    _CursorRow += rowDelta;
     
    _CursorColumn += columnDelta;
     
    KeepCursorInBounds();

      if (_ActiveViewMaximized)
        PositionViews();

     
    ShowCursor();
    }

    If Ctrl is held down and the arrow keys are pressed, I still want MoveCursor to be called. So we'll need to make a small modification to ProcessCmdKey, splitting its switch statement in two, one that switches on keyData, and the other that switches on keyData with the control key removed, like this:

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
     
    const int WM_KEYDOWN = 0x100;
     
    if (msg.Msg == WM_KEYDOWN)
     
    {
       
    switch (keyData)
        {
         
    case Keys.Enter:
           
    CloseAndPaste();
           
    return true;
         
         
    case Keys.Escape:
           
    Close();
           
    return true;

        }
        switch (keyData & ~Keys.Control)
        {

         
    case Keys.Left:
           
    MoveCursor(0, -1);
           
    return true;

         
    case Keys.Right:
           
    MoveCursor(0, 1);
           
    return true;

         
    case Keys.Up:
           
    MoveCursor(-1, 0);
           
    return true;

         
    case Keys.Down:
           
    MoveCursor(1, 0);
           
    return true;
       
    }
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

     The second switch statement strips out the Control key if present in the keyData parameter (so, for example, Ctrl+Left would be treated like a Left when execution enters the second switch).

    That should do it. After making these changes, let's give it a try...

    Testing the New CodeView Maximize/Distribute Feature

    Click Run. In the second instance of Visual Studio, press Ctrl+Shift+Insert to bring up the Clipboard History.

    Use the arrow keys to select an entry with scrollbars...

    CodeViewsDistributedEvenly

    Now hold down the Ctrl key to maximize the active view...

     ActiveCodeViewMaximized

    Nice. Now release the Ctrl key to restore the distributed view default. Try holding down the Ctrl key and using the arrow keys.

    Interact with that a bit. See if it feels right to you.

    Tomorrow we'll improve discoverability into this new Ctrl-key feature, and add a right-click context menu to our form. See you then!

5 6 7 8 9
10
11 12 13 14
LIVE CHAT

Chat is one of the many ways you can contact members of the DevExpress Team.
We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.

If you need additional product information, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners