Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Implementing Custom XAML Intellisense VS2017 Extension

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
22 Nov 2017CPOL15 min read 16.2K   196   7   1
Describes creating a XAML Intellisense Visual Studio 2017 extension

Introduction

Visual Studio 2017 finally allowed filtering suggested intellisense completions by their types (property, method, event etc) in C#, however, the same was not provided for XAML.

I work a lot with XAML and always liked the ability to narrow down the completion results to a certain type - I had to use custom extensions by Karl Shiflett and Bnaya Eshet to achieve that for the previous versions of Visual Studio.

Since VS2017 still misses this ability, I built a custom extension to par the XAML intellisense ability with C#: Image 1

The extension is available from Visual Studio Marketplace.

Programming Visual Studio Extensions (or VSIX) is a very exciting and interesting topic since a good extension can almost immediately increase your own productivity. Unfortunately VSIX part of the .NET development is very poorly documented and it is difficult to find good VSIX guides or tutorials.

The purpose of this article is to provide a guide for those C# developers who want to start building VSIX extensions.

Background

Several years ago, Karl Shiflett developed a very useful intellisense extension for XAML which (unlike the default extension) allowed true filtering of the suggested results by string and by type. The original extension was built for VS2010 and then he released another version for VS2015.

Bnaya Eshet built similar extensions for C# code for the same versions of Visual Studio.

As was mentioned above, Visual Studio 2017 finally provided the built-in intellisense popup that allows filtering by types for C#, but still misses the same functionality for XAML.

The extension described here, hopefully, fills the gap between XAML and C#.

Special thanks to Karl Shiflett - it is by looking at his extension code at XAMLIntelliSensePresenter2015 that I learned how to hook the C# code to the visual studio functionality.

Using the Extension

The extension is available at from Visual Studio Marketplace and can be installed either via the VS2017 Tools->Extensions and Updates menu: Image 2 or as stand alone downloaded VSIX file.

After successfully installing the extension, try editing a XAML file. You should see the intellisense popup with type filters at the bottom.

Hello World Intellisense VSIX Project

Before diving into the real extension code, let us create a smallest possible project that replaces the intellisense popups within VS2017 with a simplest possible popup control.

This excersize should give you a good feel on how to create a VSIX project, debug it, and hook it to the Visual Studio intellisense.

Prerequisites

In order to be able to build VSIX extensions, you need to install VS2017 SDK. See Installing VS2017 STD

Creating VSIX Project

To create the project do File -> Create New Project in Visual Studio. Then, within the opened dialog, choose "Extensibility" on the right and "VSIX Project" on the left. Call the project "HelloWorldVSIX" and press "OK":

Image 3

Click on the "source.extension.vsixmanafest" file within the solution explorer to open and modify the manifest properties.

Within "Metadata" tab on the left, set the Description of the project to "Hello World VSIX Project":

Image 4

Be careful with the manifest descriptions for your Extensions that you want to public on Visual Studio Marketplace - the short description of your app will be taken from your manifest. Because I was not careful, I had "Emtpy VSIX project" as my brief description in the beginning.

Then switch to the "Assets" tab on the left and click "New" button on the right:

Image 5

In the opened dialog window, choose "Microsoft.VisualStudio.MefComponent" as the type at the top, then "A project in current solution" as the Source and finally "HelloWorldVSIX" as the project:

Image 6

This will make the project MEF'able - you'll be able to decorate some of your properties with [Import] tag and then their values will be imported from the Visual Studio.

Before going further, add references to the following dlls assemblies to your project:

  1. Microsoft.VisualStudio.CoreUtility.dll
  2. Microsoft.VisualStudio.Language.Intellisense
  3. Microsoft.VisualStudio.Text.Data
  4. Microsoft.VisualStudio.Text.UI
  5. PresentationCore
  6. PresentationFramework
  7. System
  8. System.ComponentModel.Composition
  9. System.Xaml
  10. WindowsBase

All of the should be picked up from "Assemblies" option of the "Add Reference" dialog:

 

Image 7

Now, to hook to the Visual Studio intellisense functionality create a class HelloWorldIntellisenseProvider

Here is the barest bones Intellisense Presenter Provider class

using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Utilities;
using System.ComponentModel.Composition;

namespace HelloWorldVSIX
{
    // we have to export the class in order
    // for it to replace the VS default functionality
    [Export(typeof(IIntellisensePresenterProvider))]
    [ContentType("XAML")] // let it work on XAML files
    [ContentType("CSharp")] // let it also work on the C# files
    [Order(Before = "default")] //it will be picked up before the VS default
    [Name("XAML Intellisense Extension")]
    public class HelloWorldIntellisenseProvider :
        // should implement IIntellisensePresenterProvider
        IIntellisensePresenterProvider
    {
        public IIntellisensePresenter 
            TryCreateIntellisensePresenter(IIntellisenseSession session)
        {

            // returning null will 
            // trigger the default Intellisense provider
            return null;
        }
    }
}  

The class needs to be MEF exported (otherwise the VS won't know that it should replace the default intellisense provider). The only method of the original version of this class returns null, meaning that the Visual Studio will fall back to the default intellisense provider.

In spite of this (original version of the) class doing practically nothing, we can use it to verify that VS2017 really picks it up. In order to do it we need to debug the Visual Studio extension.

Debugging the VSIX Project

In order to debug the VSIX project, open the projects properties and choose the Debug tab on the left.

Choose "Start external program" option and browse to you VS2017 devenv.exe file (in my case it is "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\devenv.exe"):

Set the following command line arguments: "/rootsuffix Exp":

Image 8

The command line arguments: "/rootsuffix Exp" serve to run the Visual Studio is so called different Hive, meaning that it will run with its own Visual Studio extensions, so that we do not have to install our extension on the main version of the Visual Studio in order to debug it.

Let us place a breakpoint before return null; code of the HelloWorldIntellisenseProvider.TryCreateIntellisensePresenter method.

Now, let us run our project in the debugger. It should start a brand new instance of VS2017. If this is the first time you are debugging in Exp hive, it might ask you everything it asks after you install VS the first time (including which project templates you want to install etc).

Create a new project using this new instance of VS2017. This can be a WPF or a simple console project since our "Hello World" extension should work both in XAML and in C# code. I created a WPF project:

Image 9

Now open some XAML (or C#) file (I opened MainWindow.xaml) and start typing to open an intellisense popup. You should hit the breakpoint.

Image 10

Adding Real Meat to our VSIX Extension

Now it is time to do some real stuff.

So let us create HelloWorldIntellisensePresenterControl as a WPF UserControl:

Image 11

As you can see from our primitive implementation of VSIX, the HelloWorldIntellisenseProvider.TryCreateIntellisensePresenter method should return something that implements IIntellisensePresenter interface. I can add to it, that in order for the custom intellisense popup to appear it should implement IPopupIntellisensePresenter interface (which is an extension of IIntellisensePresenter interface. So, let us make our HelloWorldIntellisensePresenterControl implement IPopupIntellisensePresenter interface:

using Microsoft.VisualStudio.Language.Intellisense;
using System.Windows.Controls;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
using System;
using System.Windows;

namespace HelloWorldVSIX
{
    /// <summary>
    /// Interaction logic for HelloWorldIntellisensePresenterControl.xaml
    /// </summary>
    public partial class HelloWorldIntellisensePresenterControl :
        UserControl,
        IPopupIntellisensePresenter
    {
     ...
    }
}

All the IPopupIntellisensePresenter properties implementations are trivial aside from PresentionSpan property:

#region IPopupIntellisensePresenter IMPLEMENTATION

// returns itself as a popup
public UIElement SurfaceElement => this;

// determines where the intellisense popup is 
// being displayed
public ITrackingSpan PresentationSpan
{
    get
    {
        ...
    }
}

public PopupStyles PopupStyles => 
    PopupStyles.PositionClosest;

// for some reason it should 
// always be set to "completion".
// Otherwise the popup will not popup
public string SpaceReservationManagerName =>
    "completion";

// interllisense session which is set in 
// the constructor
public IIntellisenseSession Session =>
   CompletionSession;

Note, that since this class implements the popup itself, it should return itself as a SurfaceElement. Session property simply returns CompletionSession property of a more specific type ICompletionSession. This property is set in the constructor of the control.

Important Note: SpaceReservationManagerName property should always return "completion" string. Otherwise the popup will not show.

The complex PresentationSpan property contains some magic code which I do not quite understand and which I copied from Karl's code. Its purpose is to specify the intellisense popup location with respect to the text. I do not think it should change much from one application to another.

Here is the constructor of the control together with the CompletionSession property which provides the reference to the Visual Studio completion session within the control:

// interllisense session which is set in 
// the constructor
ICompletionSession CompletionSession { get; }

public HelloWorldIntellisensePresenterControl
(
    ICompletionSession completionSession
)
{
    this.CompletionSession = completionSession;

    InitializeComponent();
} 

In a real application the completions obtained in the beginning of a completion session (when the popup opens) do not change. The change in the results desplayed comes due to the filtering. I will explain it in more detail when discussing the real application.

What do we want to show in this popup? Let us display the text of the first suggested completion at the top and two buttons that will allow to commit or dismiss the completion session.

When the completion session is committed the completion string will be inserted into the XAML (or C#) text. If the completion session is dismissed, no changes will be done to the XAML (or C#) text.

Let us take a look at HelloWorldIntellisensePresenterControl.xaml:

<UserControl x:Class="HelloWorldVSIX.HelloWorldIntellisensePresenterControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:HelloWorldVSIX"
             Width="260"
             Height="130">
    <Grid Background="LightGray">
        <StackPanel Orientation="Horizontal"                   
                    HorizontalAlignment="Center"
                   VerticalAlignment="Top"
                   Margin="0,10">
            <TextBlock Text="First Completion: "
                       VerticalAlignment="Bottom"/>
            <TextBlock x:Name="FirstCompletionText"
                       VerticalAlignment="Center"
                        FontSize="14"
                        FontWeight="Bold"/>
        </StackPanel>

        <Button x:Name="CommitButton"
                Content="Commit"
                Width="70"
                Height="30"
                HorizontalAlignment="Left"
                VerticalAlignment="Bottom"
                Margin="20,10"/>

        <Button x:Name="DismissButton"
                Content="Dismiss"
                Width="70"
                Height="30"
                HorizontalAlignment="Right"
                VerticalAlignment="Bottom"
                Margin="20,10"/>
    </Grid>
</UserControl>  

Our control has fixed size 260x130, it has a TextBlock and two buttons discussed above.

Now let us come back to the C# code. We define FirstCompletion readonly property to contain the first completion of the session:

public Completion TheFirstCompletion { get; }

Within the HelloWorldIntellisensePresenterControl's constructor, we obtain all the completions and assign the first to our TheFirstCompletion property:

// getting all possible completions
List<Completion> allCompletions =
    this.CompletionSession
        .SelectedCompletionSet
        .Completions
        .ToList();

// we choose the fist completion
// out all possible completions
TheFirstCompletion = allCompletions.First();  

We also assign the text to be inserted to our TextBlock defined in XAML file:

// set the text of the popup
// to the text to be inserted
// in case the user clicks "Commit"
FirstCompletionText.Text = TheFirstCompletion.InsertionText;

Now we add the handlers to our buttons' Click events:

CommitButton.Click += CommitButton_Click;
DismissButton.Click += DismissButton_Click;

And the handlers' implementions are very simple:

private void CommitButton_Click(object sender, RoutedEventArgs e)
{
    // set the selection status
    CompletionSession.SelectedCompletionSet.SelectionStatus =
        new CompletionSelectionStatus
        (
            TheFirstCompletion,
            true,
            true
        );

    CompletionSession.Commit();
}

private void DismissButton_Click(object sender, RoutedEventArgs e)
{
    // even though the selection status is set
    // calling CompletionSession.Dismiss method 
    // will result in rollback of any text change.
    CompletionSession.SelectedCompletionSet.SelectionStatus =
        new CompletionSelectionStatus
        (
            TheFirstCompletion,
            true,
            true
        );

    CompletionSession.Dismiss();
}  

Note, that in general we do not always control when the CompletionSession commits (e.g. it usually commits when the user presses "Enter" key, so the SelectionStatus should be assigned when the filtered result set changes, not right before the Commit. The assignments of SelectionStatus before Commit or Dismiss was used simply to emphasize the corresponding feature.

Finally, we need to go back to HelloWorldIntellisenseProvider.cs and return our newly built control instead of null:

public IIntellisensePresenter 
    TryCreateIntellisensePresenter(IIntellisenseSession session)
{

    // returning null will 
    // trigger the default Intellisense provider
    return new HelloWorldIntellisensePresenterControl(session as ICompletionSession);
}  

Now, try debugging the application and modifying XAML or C# of the Visual Studio that starts. Here is what we get:

Image 12

Note, that in my case the first Completion object in the collection contained "AccessKeyManager" as text to insert (it is displayed at the top of the popup).

Now, if you press "Dismiss" button, the text within Grid tag will not change and if you press "Commit" button, the tag will add the same text as was shown at the top of our Intellisense popup after "<Grid". In my sample the text will be "AccessKeyManager" so the tag will now look approximately like:

<Grid AccessKeyManager >

Code Location

The code for this "HelloWorldVSIX" extension is located on GITHUB under HelloWorldVSIX repository.

Code of the Real NP.XAMLIntellisenseExtensionForVS2017 Application

A Very Important Instruction

If you are experimenting with another Intellisense extension within the same Exp hive, please, remove the installation of the previous extension by going to your folder C:/Users/<user_name>/ÅppData/Local/Microsoft/VisualStudio/<Exp_hive_folder>/Extensions and removing the folder corresponding to your previous extension - in my case I remove the whole folder "NickPolyak". Also, in my case, the <Exp_hive_folder> is called 15.0_7ff6e654Exp (but it depends on the VS version).

Kudos to Karl for mentioning this next to his GITHUB code. It would have taken me at least several hours to figure it out on my own.

Introduction

Here I describe the code of the real NP.XAMLIntellisenseExtensionForVS2017. This code is can be found under NP.XAMLIntellisenseExtensionForVS2017 url on GITHUB.

The overview of the code will not be extremely detailed - in particular I skip parts which are similar to those of the HelloWorldVSIX extension described above. I also do not do a detailed explanation of the WPF concepts and behaviors - after all this is a VSIX and not a WPF tutorial.

Code Structure and Overview of two Simple Classes

The code structure is exactly the same as that of our HelloWorldVSIX project.

Our presenter provide class is called XAMLIntellisensePresenterProvider. It is very similar to HelloWorldIntellisenseProvider except that it does not kick in for C# code - only for XAML. This is achieved by omitting [ContentType("CSharp")] from its attributes.

The other difference is that we check within its TryCreateIntellisensePresenter(...) method if there are any completions and if there are none, we return null ensuring the default behavior. I am not sure if it is even needed - I think if there are no completions most likely the method won't even get called, but I put it there just in case:

[Export(typeof(IIntellisensePresenterProvider))]
[ContentType("XAML")]
[Order(Before = "default")]
[Name("XAML Intellisense Extension")]
public class XAMLIntellisensePresenterProvider : 
    IIntellisensePresenterProvider
{
    public IIntellisensePresenter 
        TryCreateIntellisensePresenter(IIntellisenseSession session)
    {
        ICompletionSession completionSession = session as ICompletionSession;

        CompletionSet completionSet = completionSession.SelectedCompletionSet;

        IEnumerable<Completion>
            allCompletions = completionSet?.Completions.ToList();

        if ( (allCompletions == null) || 
             (allCompletions.Count() == 0) )
        {
            // ensures default behavior if there are no completions
            return null;
        }

        return new XAMLIntellisensePresenterControl(completionSession);
    }
}

Our popup up control is called XAMLIntellisensePresenterControl and consists of two classes: XAMLIntellisensePresenterControl.xaml and XAMLIntellisensePresenterControl.xaml.cs.

There are only two C# code files in the project: CompletionTypeFilter.cs - containing the View Model for the filter buttons at the bottom and ResendEventBehavior.cs containing a behavior whose purpose is to get around some unwanted behaviors of ListViewItem class.

Most of the complexity, of course is concentrated in our XAMLIntellisensePresenterControl class.

As some expert WPF readers might notice - I virtually avoided MVVM pattern here (aside from the bottom filters case). The control is too simple for using the MVVM pattern here from my point of view.

Let us start by describing the very simple CompletionTypeFilter class, since it is used within XAMLIntellisensePresenterControl class. It contains only three properties:

  1. string CompletionFilterType { get; } - is set from Completion.IconAutomationText and it uniquely specifies the type of the filter (namespace or event or property etc).
  2. ImageSource TheIcon { get; } - contains the bitmap for the filter icon image.
  3. bool IsOn { get; set; } - this is a notifiable property i.e. property firing INotifiedPropertyChanged.PropertyChanged event on change. It specifies whether the filtering on this filter type is on or not.

 

Two first properties are read-only and set from within the constructor of CompletionTypeFilter class:

public CompletionTypeFilter(string completionFilterType, ImageSource icon)
{
    CompletionFilterType = completionFilterType;
    TheIcon = icon.Clone();
}  

 

IsOn property is being set or unset when the user pushes the corresponding filter button at the bottom of the intellisense popup.

Overview of XAMLIntellisensePresenterControl class C# Code

Most of the complexity resides, of course within XAMLIntellisensePresenterControl class (both it's XAML and C# parts).

Let us start by looking at the C# code (XAMLIntellisensePresenterControl.xaml.cs file).

The implementation of IPopupIntellisensePresenter interface is located at the top of the body of the class within IPopupIntellisensePresenter_Properties_Implementation region and is exactly the same as that of the presenter control of the HelloWorldVSIX extension that was discussed above.

Just line in HelloWorldVSIX sample, this presenter class defines the read-only CompletionSession property which is set within the constructor.

There are 3 more read-only properties defined within XAMLIntellisensePresenterControl class:

// collection view that facilitates filtering
// of the completions collection
public ICollectionView TheCompletionsCollectionView { get; }

// View Model collection of the completion filters
public ObservableCollection<CompletionTypeFilter> 
    TheCompletionTypeFilters { get; }

// The text being typed by the user. 
// It is used for filtering the completion result set. 
public string UserText =>
    CompletionSession
        .SelectedCompletionSet
        ?.ApplicableTo
            .GetText
            (
                CompletionSession.SelectedCompletionSet
                                    .ApplicableTo
                                    .TextBuffer
                                    .CurrentSnapshot)?.ToLower();  

Their purpose is explained within the code comments above.

Just a reminder that ICollectionView is utilized in WPF as a means to automate filtering (as well as sorting and grouping, but here we only use filtering) of the ItemSources of ListView or ListBox controls.

Here is how the TheCompletionsCollectionView is set within the constructor:

// get all completions 
// this set does not change throughout the session
IEnumerable<Completion>
    allCompletions = 
        completionSession.SelectedCompletionSet
                         .Completions
                         .ToList();

// create the ICollectionView object
// in order to facilitate the filtering of the
// completions
TheCompletionsCollectionView =
    CollectionViewSource.GetDefaultView(allCompletions);

We get all the completions from the completion session and call CollectionViewSource.GetDefaultView(...) method. This is a typical way of obtaining ICollectionView object. The resulting ICollectionView retains the reference to allCompletions collection and uses it as a source.

Note, that the set of completions does not change throughout the session. The only thing that changes is how we filter it.

Here is how we set TheCompletionTypeFilters property:

// create a single filter for each IconAutomationText
TheCompletionTypeFilters = new ObservableCollection<CompletionTypeFilter>
(
    allCompletions
    .GroupBy(compl => compl.IconAutomationText)
    .Where(groupItem => groupItem.Key != "9") // completion tag (gets automatically added)
    .Select
    (
        groupItem => 
            new CompletionTypeFilter
            (
                groupItem.Key, 
                groupItem.First()
                         .IconSource
                         .Clone()))
);

// there is no reason to have a single filter
// changing it will show or hide 
// all the completions
if (TheCompletionTypeFilters.Count == 1)
{
    TheCompletionTypeFilters.Clear();
}

Essentially we group all completions by IconAutomationText property. This property uniquely defines the completion filter type (whether it is a property, an event etc.). For each group we create the filter copying the IconSource bitmap of the completion to it. Note that we skip the entries that have IconAutomationText set to "9" since "9" means a closing XML tag and it is created automatically when you create an opening tag.

Note that if there is only one completion type filter, we do not show it at all since there is no reason to use it - it will either show or hide all the completions within the popup.

Next we assing the filtering delegate to our CollectionView:

// set the filtering delegate
// to DoFiltering method that filters
// a completion by user text and by and by 
// the completion type filters which are in 
// 'On' state. 
TheCompletionsCollectionView.Filter = DoFiltering;

Here is DoFiltering(...) method implementation:

// filter a completion first by UserText and 
// then by completion filters
bool DoFiltering(object obj)
{
    Completion completion = (Completion) obj;

    string userText = UserText;

    // filter by user text (if text does not match return 'false')
    if ((userText != null) && (completion.DisplayText?.ToLower()?.Contains(userText) != true))
        return false;

    IEnumerable<CompletionTypeFilter> completionFiltersThatAreOn =
        TheCompletionTypeFilters.Where(filt => filt.IsOn).ToList();

    // filter by completion filters that are in 'On' state
    return
        (completionFiltersThatAreOn.Count() == 0) ||  
        completionFiltersThatAreOn.FirstOrDefault
           (
               filt => filt.CompletionFilterType == completion.IconAutomationText
           ) != null;
}  

Every time the current item changes within our CollectionView, we want to set the CompletionSession.SelectedCompletionSet.SelectionStatus to point to it, so that if session commit comes next, it will use the selected completion. This is why we are assinging a handler to ICollectionView.CurrentChanged event:

// set the completion status to 
// the current completion every time 
// the Current item changes with the 
// collection view
TheCompletionsCollectionView.CurrentChanged +=
    TheCompletionsCollectionView_CurrentChanged;  

And here is the handler implementation:

// set the SelectionStatus to the 
// current item of the CollectionView
private void TheCompletionsCollectionView_CurrentChanged
(
    object sender, 
    EventArgs e
)
{
    object selectedItem =
        TheCompletionsCollectionView.CurrentItem;

    if (selectedItem != null)
    {
        if (CompletionSession.SelectedCompletionSet != null)
        {
            try
            {
                // sometimes it throws an unclear exception
                // so placed it within try/catch block
                CompletionSession
                    .SelectedCompletionSet
                    .SelectionStatus =
                        new CompletionSelectionStatus
                        (
                            selectedItem as Completion, 
                            true, 
                            true
                        );
            }
            catch
            {
            }
        }
    }
}

We need to force refiltering every time a filter's state changes IsOn property changes. This is how it is done:

// force the refreshing of the view
// each time any filter's state changes
foreach (CompletionTypeFilter filter in TheCompletionTypeFilters)
{
    filter.PropertyChanged += Filter_PropertyChanged;
}

And the all Filter_PropertyChanged handler does is calling the refresh on the view, thus forcing a refiltering:

private void Filter_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    TheCompletionsCollectionView.Refresh();
}  

Then, still within the XAMLIntellisensePresenterControl constructor, we call SelectItemBasedOnTextFiltering to set the initial CompletedStatus based on the UserText:

void SelectItemBasedOnTextFiltering()
{
    string userText = UserText;

    bool foundCompletion = false;
    // if we find completion that starts with the text
    // we choose it. 
    if (!string.IsNullOrEmpty(userText))
    {
        foreach (Completion completion in TheCompletionsCollectionView)
        {
            if (completion.DisplayText?.ToLower().StartsWith(userText) == true)
            {
                SelectItem(completion);
                foundCompletion = true;
                break;
            }
        }
    }

    // if the match by text was not found
    // we move the current item to the first of
    // items within the filtered collection
    if (!foundCompletion)
    {
        TheCompletionsCollectionView.MoveCurrentToFirst();
    }

    // we force the ListView to scroll to the 
    // current item
    ScrollAsync();
}  

We are also setting several more event handlers within the constructor.

We, possibly, need to choose another item when user text changes, this is achieved by setting TextBuffer.Change event handler:

// when user text changes,
// re-filter and (possibly)
// choose another Completion item for
// the CompletionStatus of the session
CompletionSession.TextView.TextBuffer.Changed +=
    TextBuffer_Changed;  
private void TextBuffer_Changed(object sender, TextContentChangedEventArgs e)
{
    // refresh the filter
    TheCompletionsCollectionView.Refresh();

    // choose the CompletionStatus based on the new filtering
    SelectItemBasedOnTextFiltering();
}  

Next we set the handler for ResendEventBehavior.CustomEvent. This event is fired by ResendEventBehavior when the user clickes on a ListViewItem. The reason for this event was that MouseDown event was consumed by the ListViewItem and not propagated up the visual tree to the whole control. The purpose of the handler TheCompletionsListView_MouseDown is to select as current item the completion corresponding to the clicked ListViewItem.

Also on mouse double click we want to commit the session:

TheCompletionsListView.MouseDoubleClick += 
    TheCompletionsListView_MouseDoubleClick;  
private void TheCompletionsListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    CompletionSession?.Commit();
}  

Note that our control XAMLIntellisensePresenterControl does not just implement IPopupIntellisensePresenter, it also implements IPopupIntellisensePresenter interface. This interface has only one method ExecuteKeyboardCommand that facilitates dealing with the keyboard commands. Here is my implementation of this method:

public bool ExecuteKeyboardCommand(IntellisenseKeyboardCommand command)
{
    switch (command)
    {
        case IntellisenseKeyboardCommand.Up:
            MoveCurrentByIdx(-1);
            return true;
        case IntellisenseKeyboardCommand.PageUp:
            MoveCurrentByIdx(-10);
            return true;
        case IntellisenseKeyboardCommand.Down:
            MoveCurrentByIdx(1);
            return true;
        case IntellisenseKeyboardCommand.PageDown:
            MoveCurrentByIdx(10);
            return true;
        case IntellisenseKeyboardCommand.Escape:
            this.CompletionSession.Dismiss();
            return true;
        default:
            return false;
    }
}

MoveCurrentByIdx moves the current item of the ICollectionView by the integer number passed to it. If such index does not exist within the CollectionView it will move it as far as it can - to the first or last item of the CollectionView.

You may note that I am not doing anything to IntellisenseKeyboardCommand.Enter key. This is because the "Enter" key is wired to commit the session and close the popup. I do not think there is an easy way of changing this behavior.

Overview of XAMLIntellisensePresenterControl class XAML Code

Now, let us take a brief peek at parts of the XAML file XAMLIntellisensePresenterControl.xaml.

here is the code for the Completions ListView with some comment in it:

<ListView x:Name="TheCompletionsListView"
          BorderBrush="Orange"
          ItemsSource="{Binding Path=TheCompletionsCollectionView, 
                                RelativeSource={RelativeSource AncestorType=UserControl}}"
          IsSynchronizedWithCurrentItem="True"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          ScrollViewer.HorizontalScrollBarVisibility="Disabled"
          Width="300"
          Height="200">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="Padding"
                    Value="0"/>
            <Setter Property="HorizontalContentAlignment"
                    Value="Stretch"/>
            <!-- make ListViewItem non-focusable and set its behavior
                 in order to make the MouseDown event propagate 
                 over to the whole popup-->
            <Setter Property="Focusable"
                    Value="False"/>
            <Setter Property="local:ResendEventBehavior.TheResendEventBehavior">
                <Setter.Value>
                    <local:ResendEventBehavior TheRoutedEvent="{x:Static FrameworkElement.MouseDownEvent}"/>
                </Setter.Value>
            </Setter>
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid x:Name="TheItemTopPanel"
                  Background="Transparent">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <!-- Completion Icon -->
                <Image Source="{Binding Path=IconSource}"
                       Margin="2,0,5,0"
                       Width="13"
                       Height="13"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"/>
                <!-- Completion Text -->
                <TextBlock x:Name="TheCompletionText"
                           Text="{Binding Path=DisplayText}" 
                           Grid.Column="1"
                           Margin="0,0,20,0"/>
            </Grid>
            <DataTemplate.Triggers>
                <!-- change the item foreground and background when selected-->
                <DataTrigger Binding="{Binding Path=IsSelected, 
                                               RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
                             Value="True">
                    <Setter TargetName="TheItemTopPanel"
                            Property="Background"
                            Value="#FF007ACC"/>
                    <Setter TargetName="TheCompletionText"
                            Property="Foreground"
                            Value="White"/>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

And here is the code for the completion type filters ItemsControl also reasonably documented in code:

<ItemsControl x:Name="TheCompletionTypeFilters"
          Focusable="False"
          HorizontalAlignment="Left"
          VerticalAlignment="Center"
          ItemsSource="{Binding Path=TheCompletionFilters, 
                                RelativeSource={RelativeSource AncestorType=UserControl}}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- arrange the filters horizontally -->
            <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <!-- CheckBox.IsChecked property is connected to the 
                 IsOn property on the CompletionTypeFilter view model -->
            <CheckBox IsChecked="{Binding Path=IsOn, 
                                          Mode=TwoWay}">
                <CheckBox.Template>
                    <ControlTemplate TargetType="CheckBox">
                        <Grid x:Name="FilterItemPanel" 
                              Width="20"
                              Height="20">
                            <!-- filter icon -->
                            <Image HorizontalAlignment="Center"
                                   VerticalAlignment="Center"
                                   Width="13"
                                   Height="13"
                                   Source="{Binding Path=TheIcon}"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <!-- change filter look on mouse over -->
                            <Trigger Property="IsMouseOver"
                                     Value="True">
                                <Setter TargetName="FilterItemPanel"
                                        Property="Background"
                                        Value="#FF007ACC"/>
                            </Trigger>

                            <!-- change filter look when IsOn == true -->
                             <DataTrigger Binding="{Binding Path=IsOn}"
                                         Value="True">
                                <Setter TargetName="FilterItemPanel"
                                        Property="Background"
                                        Value="#FF007ACC"/>
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </CheckBox.Template>
            </CheckBox>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>  

Conclusion

In this article I explain how to create intellisense extensions for the VS 2017.

Appendix

Installing VS2017 SDK

You can install VS2017 SDK by choosing the appropriate option during VS2017 installation.

If, however, you already have VS2017 installed you can go to "Progams and Features" (or "Apps and Features" on Windows 10) control panel menu find VS2017 there and click "Modify" option: Image 13

After waiting till all the updates are downloaded, click "Modify" button on the Visual Studio 2017 window Image 14

Look on the right hand side under "Visual Studio extension development". Check "Visual Studio SDK" and "Visual Studio extension development prerequisites" options and click "Modify" at the bottom right corner (or if they are already checked, you do not need to do anything - they've been already installed): Image 15.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
QuestionNP.XAMLIntellisenseExtensionForVS2017 build in vs 2017. I'm getting the below error Pin
Member 1047600711-Nov-19 2:08
professionalMember 1047600711-Nov-19 2:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.