in
Forums
Blogs
Files
Devexpress.Com
ClientCenter
Support Center
DevExpress Channel

Mark Miller

  • 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. Th