Creating Your Own MEF-Based Editor Extension

You have now learned all the ingredients necessary to build your own extension. Let’s walk through a simple example, end to end, and build an extension that displays some basic code metrics in a window in the corner of the editor. Functionally, you need to accomplish the following:

Image Compute the required code stats by parsing the currently loaded code file

Image Expose a set of properties on a WPF user control to hold those metrics (and which also displays those metrics)

Image Display the WPF user control as an editor viewport adornment

Because this involves creating a new adornment pegged to the editor’s viewport, you have the luxury of starting with the code produced for us by the Viewport Adornment template.

Create a new project called CodeMetricAdornment using the VSIX project template. Then add a new Editor Viewport Adornment item to the project and call it CodeMetricViewportAdornment. Finally, you will add a WPF user control titled CodeMetricDisplayControl. (If you are unfamiliar with WPF or XAML development in general, you may want to read through portions of Chapter 21, “Building WPF Applications.”)

Your baseline project structure should look like Figure 16.8.

Image

FIGURE 16.8 Creating a new adornment project.

Now open the CodeMetricViewportAdornment class itself. Within the class, you add a field to hold a WPF user control object, and you get rid of the current Image field that the template code uses.

private CodeMetricDisplayControl _displayControl =
            new CodeMetricDisplayControl();

We’ll get to our code counting algorithm in a bit. Let’s first focus on creating the content within our user control (CodeMetricDisplayControl). We need to display the three metrics and their three labels within the control; a grid works nicely for this.

Because the grid is already included by default as the root arrangement element of the control, you just need to tweak its display a bit. Add three rows and two columns to the grid.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
</Grid>

Within the grid, add six TextBlock objects—three to hold the labels, and three to hold the actual counts.

<TextBlock Grid.Column="0" Grid.Row="0"
           VerticalAlignment="Center"
           x:Name="TextBlockLOC"
           Padding="5">Total Lines of Code:</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="1"
           x:Name="TextBlockComments"
           VerticalAlignment="Center"
           Padding="5">Comment Lines:</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="2"
           x:Name="TextBlockWhitespace"
           VerticalAlignment="Center"
           Padding="5">Whitespace Lines:</TextBlock>

<TextBlock Grid.Column="1" Grid.Row="0"
           VerticalAlignment="Center">0</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1"
           VerticalAlignment="Center">0</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="2"
           VerticalAlignment="Center">0</TextBlock>

Within the CodeMetricDisplayControl class, add a property for each of these integers as well.

public partial class CodeMetricDisplayControl : UserControl
{
    private int _loc = 0;   //total lines of code
    private int _whitespace = 0; //whitespace (empty) lines
    private int _comments = 0; //total lines that are comments

    public int LinesOfCode
    {
        get { return _loc; }
        set { _loc = value; Refresh(); }
    }


    public int CommentLines
    {
        get { return _comments; }
        set { _comments = value; Refresh(); }
    }


    public int WhitespaceLines
    {
        get { return _whitespace; }
        set { _whitespace = value; Refresh(); }
    }

    public CodeMetricDisplayControl()
    {
        InitializeComponent();
    }

}

Note that, in each setter, you are calling a Refresh routine. Let’s write that routine, which updates the value of our TextBlocks to the current value held by our field for each of the code metrics.

private void Refresh()
{
    this.TextBlockComments.Text = _comments.ToString();
    this.TextBlockLOC.Text = _loc.ToString();
    this.TextBlockWhitespace.Text = _whitespace.ToString();
}

Disregarding any look and feel refinements, the WPF user control is now ready to go. Let’s turn our attention back to the adorner class and write the code to actually count our lines of code, comments, and whitespace lines.

In CodeMetricViewportAdornment, strip out all the current drawing code from the constructor. In its place, add three lines that set the user control’s properties based on the return values from a few private routines (which we implement next). The constructor should now look like this:

public CodeMetricAdornment(IWpfTextView view)
{
    _view = view;

    _displayControl.LinesOfCode = CountLOC(view);

    _adornmentLayer =
        view.GetAdornmentLayer
        ("CodeMetricAdornment");

    _view.ViewportHeightChanged += delegate { this.onSizeChange(); };
    _view.ViewportWidthChanged += delegate { this.onSizeChange(); };
 }

The code to actually count our lines of code is straightforward. We take the IWpfTextView object provided by our adorner and get the string representation for all the current text in the editor window like this:

string code = view.TextSnapshot.GetText();

Now we can parse that string and return the various counts.

private int CountLOC(IWpfTextView view)
{
   string code = view.TextSnapshot.GetText();

   int count = 1;
   int start = 0;
   while ((start = code.IndexOf(' ', start)) != -1)
   {
       count++;
       start++;
   }

    return count;


}

private int CountWhitespaceLines(IWpfTextView view)
{
    string code = view.TextSnapshot.GetText();
    int count = 0;

     using (StringReader reader = new StringReader(code))
    {
        string line;

        while ((line = reader.ReadLine()) != null)
        {
            if (line.Trim() == "")
                count++;
        }

        return count;
     }
}

private int CountCommentLines(IWpfTextView view)
{
    string code = view.TextSnapshot.GetText();
    int count = 0;

    using (StringReader reader = new StringReader(code))
    {
        string line;

        while ((line = reader.ReadLine()) != null)
        {
            if (line.TrimStart().StartsWith("//"))
                count++;
        }

        return count;
     }
}

We are almost done. The final piece that we need to change from the template code is to rewrite a piece of the OnSizeChange event handler. This was wired to position and display the WPF Image control originally used by the template. Instead, we want this code to position and place our WPF user control. Change the code within OnSizeChange to this:

public void onSizeChange()
    {
        //Clear the adornment layer of previous adornments
        _adornmentLayer.RemoveAllAdornments();

        //Place the image in the top-right corner of the viewport
        Canvas.SetLeft(_displayControl,
            _view.ViewportRight - _displayControl.ActualWidth);
        Canvas.SetTop(_displayControl,
            _view.ViewportTop + _displayControl.ActualHeight);

        //Add the image to the adornment layer and make it relative to the
        //viewport
        _adornmentLayer.AddAdornment(
            AdornmentPositioningBehavior.ViewportRelative,
            null, null, _displayControl, null);
    }

Now the control is positioned and is in the adornment view layer. If you run the project and then open a code file from within the IDE instance that is launched, you should immediately see the fruits of your labor in the upper-right corner of the editor.

There is still one problem with this implementation. Although the code stats are displayed correctly, they aren’t updated when we change the text within the editor. To fix this, we need to react to the adornment layer’s IWpfTextView LayoutChanged event. We can hook this event inside our CodeMetricViewportAdornment constructor like this.

_view.LayoutChanged += this.OnLayoutChanged;

And then we create the event handler that updates our counts.

private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
    _displayControl.LinesOfCode = CountLOC(_view);
    _displayControl.CommentLines = CountCommentLines(_view);
    _displayControl.WhitespaceLines = CountWhitespaceLines(_view);
}

We can make our WPF user control more compelling to look at by tweaking the XAML to add things like a background gradient and text coloring.

Figure 16.9 shows the final product, and Listing 16.1 contains the viewport adornment class code. The full source listing for this project is available at this book’s website: http://informit.com/title/9780672337369.

Image

FIGURE 16.9 A simple code metric adornment in the editor.

LISTING 16.1 The CodeMetricViewportAdornment Class


using System.IO;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Editor;

namespace CodeMetricViewportAdornment
{
    /// <summary>
    /// Adornment class that draws a square box in the top-right corner of
    /// the viewport
    /// </summary>
    class CodeMetricAdornment
    {
        private IWpfTextView _view;
        private IAdornmentLayer _adornmentLayer;
        private CodeMetricDisplayControl _displayControl =
            new CodeMetricDisplayControl();


        public CodeMetricAdornment(IWpfTextView view)
        {
            _view = view;

            _displayControl.LinesOfCode = CountLOC(view);
            _displayControl.CommentLines = CountCommentLines(view);
            _displayControl.WhitespaceLines = CountWhitespaceLines(view);

            _adornmentLayer = view.GetAdornmentLayer("CodeMetricAdornment");

            _view.LayoutChanged += this.OnLayoutChanged;
            _view.ViewportHeightChanged += delegate { this.onSizeChange(); };
            _view.ViewportWidthChanged += delegate { this.onSizeChange(); };
        }

        public void onSizeChange()
        {
            //Clear the adornment layer of previous adornments
            _adornmentLayer.RemoveAllAdornments();

            int buffer = 50;

            //Place the image in the top-right corner of the viewport
            Canvas.SetLeft(_displayControl,
                _view.ViewportRight - (_displayControl.ActualWidth
                + buffer));
            Canvas.SetTop(_displayControl,
                _view.ViewportTop + (_displayControl.ActualHeight
                + buffer));

            _adornmentLayer.AddAdornment(
                AdornmentPositioningBehavior.ViewportRelative,
                null, null, _displayControl, null);
        }

        private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs
e)
        {
            _displayControl.LinesOfCode = CountLOC(_view);
            _displayControl.CommentLines = CountCommentLines(_view);
            _displayControl.WhitespaceLines = CountWhitespaceLines(_view);
        }

        private int CountLOC(IWpfTextView view)
        {
           string code = view.TextSnapshot.GetText();

           int count = 1;
           int start = 0;
           while ((start = code.IndexOf(' ', start)) != -1)
           {
               count++;
               start++;
           }

            return count;

        }

        private int CountWhitespaceLines(IWpfTextView view)
        {
            string code = view.TextSnapshot.GetText();
            int count = 0;

            using (StringReader reader = new StringReader(code))
            {
                string line;

                while ((line = reader.ReadLine()) != null)
                {
                    if (line.Trim() == "")
                        count++;
                }

                return count;

            }
        }

        private int CountCommentLines(IWpfTextView view)
        {
            string code = view.TextSnapshot.GetText();
            int count = 0;

            using (StringReader reader = new StringReader(code))
            {
                string line;

                while ((line = reader.ReadLine()) != null)
                {
                    if (line.TrimStart().StartsWith("//"))
                        count++;
                }

                return count;

            }
        }
    }
}


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset