Share via


Walkthrough: Creating a SharePoint Project Extension

This walkthrough illustrates how to create an extension for SharePoint projects. You can use a project extension to respond to project-level events such as when a project is added, deleted, or renamed. You can also add custom properties or respond when a property value changes. Unlike project item extensions, project extensions cannot be associated with a particular SharePoint project type. When you create a project extension, the extension loads when any kind of SharePoint project is opened in Visual Studio.

In this walkthrough, you will create a custom Boolean property that is added to any SharePoint project created in Visual Studio. When set to True, the new property adds, or maps, an Images resource folder to your project. When set to False, the Images folder is removed, if it exists. For more information, see How to: Add and Remove Mapped Folders.

This walkthrough demonstrates the following tasks:

  • Creating a Visual Studio extension for SharePoint projects that does the following:

    • Adds a custom project property to the Properties window. The property applies to any SharePoint project.

    • Uses the SharePoint project object model to add a mapped folder to a project.

    • Uses the Visual Studio automation object model (DTE) to delete a mapped folder from the project.

  • Building a Visual Studio Extension (VSIX) package to deploy the project property's extension assembly.

  • Debugging and testing the project property.

Prerequisites

You need the following components on the development computer to complete this walkthrough:

Creating the Projects

To complete this walkthrough, you must create two projects:

  • A VSIX project to create the VSIX package to deploy the project extension.

  • A class library project that implements the project extension.

Start the walkthrough by creating the projects.

To create the VSIX project

  1. Start Visual Studio.

  2. On the menu bar, choose File, New, Project.

  3. In the New Project dialog box, expand the Visual C# or Visual Basic nodes, and then choose the Extensibility node.

    Note

    This node is available only if you install the Visual Studio SDK. For more information, see the prerequisites section earlier in this topic.

  4. At the top of the dialog box, choose .NET Framework 4.5 in the list of versions of the .NET Framework, and then choose the VSIX Project template.

  5. In the Name box, enter ProjectExtensionPackage, and then choose the OK button.

    The ProjectExtensionPackage project appears in Solution Explorer.

To create the extension project

  1. In Solution Explorer, open the shortcut menu for the solution node, choose Add, and then choose New Project.

    Note

    In Visual Basic projects, the solution node appears in Solution Explorer only if the Always show solution check box is selected in the General, Projects and Solutions, Options Dialog Box.

  2. In the New Project dialog box, expand the Visual C# or Visual Basic nodes, and then choose Windows.

  3. At the top of the dialog box, choose .NET Framework 4.5 in the list of versions of the .NET Framework, and then choose the Class Library project template.

  4. In the Name box, enter ProjectExtension, and then choose the OK button.

    Visual Studio adds the ProjectExtension project to the solution and opens the default Class1 code file.

  5. Delete the Class1 code file from the project.

Configuring the Project

Before you write code to create the project extension, add code files and assembly references to the extension project.

To configure the project

  1. Add a code file that's named CustomProperty to the ProjectExtension project.

  2. Open the shortcut menu for the ProjectExtension project, and then choose Add Reference.

  3. In the Reference Manager – CustomProperty dialog box, choose the Framework node, and then select the check box next to the System.ComponentModel.Composition and System.Windows.Forms assemblies.

  4. Choose the Extensions node, select the check box next to the Microsoft.VisualStudio.SharePoint and EnvDTE assemblies, and then choose the OK button.

  5. In Solution Explorer, under the References folder for the ProjectExtension project, choose EnvDTE.

  6. In the Properties window, change the Embed Interop Types property to False.

Defining the New SharePoint Project Property

Create a class that defines the project extension and the behavior of the new project property. To define the new project extension, the class implements the ISharePointProjectExtension interface. Implement this interface whenever you want to define an extension for a SharePoint project. Also, add the ExportAttribute to the class. This attribute enables Visual Studio to discover and load your ISharePointProjectExtension implementation. Pass the ISharePointProjectExtension type to the attribute's constructor.

To define the new SharePoint project property

  • Paste the following code into the CustomProperty code file.

    Imports System
    Imports System.Linq
    Imports System.ComponentModel
    Imports System.ComponentModel.Composition
    Imports System.Windows.Forms
    Imports Microsoft.VisualStudio.SharePoint
    Imports EnvDTE
    
    Namespace Contoso.SharePointProjectExtensions.MapImagesFolder
    
        ' Export attribute: Enables Visual Studio to discover and load this extension. 
        ' MapImagesFolderProjectExtension class: Adds a new Map Images Folder property to any SharePoint project.
        <Export(GetType(ISharePointProjectExtension))> _
        Public Class MapImagesFolderProjectExtension
            Implements ISharePointProjectExtension
    
            Public Sub Initialize(ByVal projectService As ISharePointProjectService) Implements ISharePointProjectExtension.Initialize
                AddHandler projectService.ProjectPropertiesRequested, AddressOf Me.projectService_ProjectPropertiesRequested
            End Sub 
    
            Private Sub projectService_ProjectPropertiesRequested(ByVal sender As Object, ByVal e As SharePointProjectPropertiesRequestedEventArgs)
                Dim propertiesObject As CustomProjectProperties = Nothing 
    
                ' If the properties object already exists, get it from the project's annotations. 
                If False = e.Project.Annotations.TryGetValue(propertiesObject) Then 
                    ' Otherwise, create a new properties object and add it to the annotations.
                    propertiesObject = New CustomProjectProperties(e.Project)
                    e.Project.Annotations.Add(propertiesObject)
                End If
    
                e.PropertySources.Add(propertiesObject)
            End Sub 
        End Class 
    
        Public Class CustomProjectProperties
            Private sharePointProject As ISharePointProject = Nothing 
            Private Const MapImagesFolderPropertyDefaultValue As Boolean = False 
            Private Const MapImagesFolderPropertyId = "ContosoMapImagesFolderProperty" 
    
            Public Sub New(ByVal myProject As ISharePointProject)
                sharePointProject = myProject
            End Sub 
    
            ' Represents the new boolean property MapImagesFolder. 
            ' True = Map an Images folder to the project if one does not already exist; otherwise, do nothing. 
            ' False = Remove the Images folder from the project, if one exists; otherwise, do nothing.
            <DisplayName("Map Images Folder")> _
            <DescriptionAttribute("Specifies whether an Images folder is mapped to the SharePoint project.")> _
            <DefaultValue(MapImagesFolderPropertyDefaultValue)> _
            Public Property MapImagesFolder As Boolean 
                Get 
                    Dim propertyStringValue As String = String.Empty
    
                    ' Try to get the current value from the .user file; if it does not yet exist, return a default value. 
                    If Not sharePointProject.ProjectUserFileData.TryGetValue(MapImagesFolderPropertyId, propertyStringValue) Then 
                        Return MapImagesFolderPropertyDefaultValue
                    Else 
                        Return CBool(propertyStringValue)
                    End If 
                End Get 
    
                Set(ByVal value As Boolean)
                    If value Then 
                        If Not ImagesMappedFolderInProjectExists(sharePointProject) Then 
                            ' An Images folder is not mapped to the project, so map one. 
                            Dim mappedFolder As IMappedFolder = sharePointProject.MappedFolders.Add(MappedFolderType.Images)
                            sharePointProject.ProjectService.Logger.WriteLine( _
                                mappedFolder.Name & " mapped folder added to the project.", LogCategory.Status)
                        End If 
                    ElseIf (ImagesMappedFolderInProjectExists(sharePointProject) AndAlso UserSaysDeleteFile()) Then 
                        ' An Images folder is mapped to the project and the user wants to remove it.
                        DeleteFolder()
                    End If
    
                    sharePointProject.ProjectUserFileData(MapImagesFolderPropertyId) = value.ToString()
                End Set 
            End Property 
    
            Private Function ImagesMappedFolderInProjectExists(ByVal sharePointProject As ISharePointProject) As Boolean 
                Dim returnValue As Boolean = False 
                For Each folder As IMappedFolder In sharePointProject.MappedFolders
                    ' Check to see if an Images folder is already mapped. 
                    If (folder.FolderType = MappedFolderType.Images) Then
                        returnValue = True 
                    End If 
                Next 
                Return returnValue
            End Function 
    
            Private Function UserSaysDeleteFile() As Boolean 
                ' Ask the user whether they want to delete the Images folder. 
                Dim returnValue As Boolean = False 
                If (MessageBox.Show("Do you want to delete the Images folder from the project?", _
                    "Delete the Images folder?", MessageBoxButtons.YesNo) = DialogResult.Yes) Then
                    returnValue = True 
                End If 
                Return returnValue
            End Function 
    
            Private Sub DeleteFolder()
                ' The Visual Studio DTE object model is required to delete the mapped folder. 
                Dim dteProject As EnvDTE.Project = _
                    sharePointProject.ProjectService.Convert(Of ISharePointProject, EnvDTE.Project)(sharePointProject)
                Dim targetFolderName As String = _
                    sharePointProject.MappedFolders.First(Function(mf) mf.FolderType = MappedFolderType.Images).Name
                Dim mappedFolderItem As EnvDTE.ProjectItem = dteProject.ProjectItems.Item(targetFolderName)
                mappedFolderItem.Delete()
    
                sharePointProject.ProjectService.Logger.WriteLine("Mapped Folder " & _
                    targetFolderName & " deleted", LogCategory.Status)
            End Sub 
        End Class 
    End Namespace
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel;
    using System.ComponentModel.Composition;
    using System.Windows.Forms;
    using Microsoft.VisualStudio.SharePoint;
    using EnvDTE;
    
    // Adds a new property called MapImagesFolder to any SharePoint project. 
    // When MapImagesFolder is set to true, the Image folder is mapped to the project. 
    // When MapImagesFolder is set to false, the Image folder is deleted from the project. 
    namespace SP_Project_Extension
    {
        // Export attribute: Enables Visual Studio to discover and load this extension.
        [Export(typeof(ISharePointProjectExtension))]
    
        // Defines a new custom project property that applies to any SharePoint project. 
        public class SPProjectExtension : ISharePointProjectExtension
        {
            // Implements ISharePointProjectService.Initialize, which determines the behavior of the new property. 
            public void Initialize(ISharePointProjectService projectService)
            {
                // Handle events for when a project property is changed.
                projectService.ProjectPropertiesRequested +=
                    new EventHandler<SharePointProjectPropertiesRequestedEventArgs>(projectService_ProjectPropertiesRequested);
            }
    
            void projectService_ProjectPropertiesRequested(object sender, SharePointProjectPropertiesRequestedEventArgs e)
            {
                // Add a new property to the SharePoint project.
                e.PropertySources.Add((object)new ImagesMappedFolderProperty(e.Project));
            }
        }
    
        public class ImagesMappedFolderProperty
        {
            ISharePointProject sharePointProject = null;
            public ImagesMappedFolderProperty(ISharePointProject myProject)
            {
                sharePointProject = myProject;
            }
            static bool MapFolderSetting = false;
    
            [DisplayName("Map Images Folder")]
            [DescriptionAttribute("Specifies whether an Images folder is mapped to the SharePoint project.")]
            public bool MapImagesFolder
            // Represents the new boolean property MapImagesFolder. 
            // True = Map an Images folder to the project if one does not already exist; otherwise, do nothing. 
            // False = Remove the Images folder from the project, if one exists; otherwise, do nothing.
            {
                get
                {
                    // Get the current property value. 
                    return MapFolderSetting;
                }
                set
                {
                    if (value)
                    {
                        if (!ImagesMappedFolderInProjectExists(sharePointProject))
                        {
                            // An Images folder is not mapped to the project, so map one.
                            IMappedFolder mappedFolder1 = sharePointProject.MappedFolders.Add(MappedFolderType.Images);
                            // Add a note to the logger that a mapped folder was added.
                            sharePointProject.ProjectService.Logger.WriteLine("Mapped Folder added:" + mappedFolder1.Name, LogCategory.Status);
                        }
                    }
                    else
                    {
                        if (ImagesMappedFolderInProjectExists(sharePointProject) && UserSaysDeleteFile())
                        {
                            // An Images folder is mapped to the project and the user wants to remove it. 
                            // The Visual Studio DTE object model is required to delete the mapped folder. 
                            // Reference the Visual Studio DTE model, get handles for the SharePoint project and project items.
                            EnvDTE.Project dteProject = sharePointProject.ProjectService.Convert<ISharePointProject, EnvDTE.Project>(sharePointProject);
                            string targetFolderName = sharePointProject.MappedFolders.First(mf => mf.FolderType == MappedFolderType.Images).Name;
                            EnvDTE.ProjectItem mappedFolderItem = dteProject.ProjectItems.Item(targetFolderName);
                            mappedFolderItem.Delete();
                            sharePointProject.ProjectService.Logger.WriteLine("Mapped Folder " + targetFolderName + " deleted", LogCategory.Status);
                        }
                    }
                    MapFolderSetting = value;
                }
    
            }
    
            private bool ImagesMappedFolderInProjectExists(ISharePointProject sharePointProject)
            {
                bool retVal = false;
                foreach (IMappedFolder folder in sharePointProject.MappedFolders)
                {
                    // Check to see if an Images folder is already mapped. 
                    if (folder.FolderType == MappedFolderType.Images)
                        retVal = true;
                }
                return retVal;
            }
    
            private bool UserSaysDeleteFile()
            {
                // Prompt the user whether they want to delete the Images folder. 
                bool retVal = false;
                if (MessageBox.Show("Do you want to delete the Images folder from the project?", "Delete the Images folder?", MessageBoxButtons.YesNo) == DialogResult.Yes)
                {
                    retVal = true;
                }
                return retVal;
    
            }
        }
    }
    

Building the Solution

Next, build the solution to make sure that it compiles without errors.

To build the solution

  • On the menu bar, choose Build, Build Solution.

Creating a VSIX Package to Deploy the Project Property Extension

To deploy the project extension, use the VSIX project in your solution to create a VSIX package. First, configure the VSIX package by modifying the source.extension.vsixmanifest file that is included in the VSIX project. Then, create the VSIX package by building the solution.

To configure and create the VSIX package

  1. In Solution Explorer, open the shortcut menu for the source.extension.vsixmanifest file, and then choose the Open button.

    Visual Studio opens the file in the manifest designer. The information that appears in the Metadata tab also appears in the Extensions and Updates. All VSIX packages require the extension.vsixmanifest file. For more information about this file, see VSIX Extension Schema 1.0 Reference.

  2. In the Product Name box, enter Custom Project Property.

  3. In the Author box, enter Contoso.

  4. In the Description box, enter A custom SharePoint project property that toggles the mapping of the Images resource folder to the project.

  5. Choose the Assets tab, and then choose the New button.

    The Add New Asset dialog box appears.

  6. In the Type list, choose Microsoft.VisualStudio.MefComponent.

    Note

    This value corresponds to the MEFComponent element in the extension.vsixmanifest file. This element specifies the name of an extension assembly in the VSIX package. For more information, see MEFComponent Element (VSX Schema).

  7. In the Source list, choose the A project in current solution option button.

  8. In the Project list, choose ProjectExtension.

    This value identifies the name of the assembly that you're building in the project.

  9. Choose OK to close the Add New Asset dialog box.

  10. On the menu bar, choose File, Save All when you finish, and then close the manifest designer.

  11. On the menu bar, choose Build, Build Solution, and then make sure that the project compiles without errors.

  12. In Solution Explorer, open the shortcut menu for the ProjectExtensionPackage project, and choose the Open Folder in File Explorer button.

  13. In File Explorer, open the build output folder for the ProjectExtensionPackage project, and then verify that the folder contains a file that's named ProjectExtensionPackage.vsix.

    By default, the build output folder is the ..\bin\Debug folder under the folder that contains your project file.

Testing the Project Property

You're now ready to test the custom project property. It's easiest to debug and test the new project property extension in an experimental instance of Visual Studio. This instance of Visual Studio is created when you run a VSIX or other extensibility project. After you debug the project, you can install the extension on your system and then continue to debug and test it in a regular instance of Visual Studio.

To debug and test the extension in an experimental instance of Visual Studio

  1. Restart Visual Studio with administrative credentials, and then open the ProjectExtensionPackage solution.

  2. Start a debug build of your project either by choosing the F5 key or, on the menu bar, choosing Debug, Start Debugging.

    Visual Studio installs the extension to %UserProfile%\AppData\Local\Microsoft\VisualStudio\11.0Exp\Extensions\Contoso\Custom Project Property\1.0 and starts an experimental instance of Visual Studio.

  3. In the experimental instance of Visual Studio, create a SharePoint project for a farm solution, and use the default values for the other values in the wizard.

    1. On the menu bar, choose File, New, Project.

    2. At the top of the New Project dialog box, choose .NET Framework 3.5 in the list of versions of the .NET Framework.

      SharePoint tool extensions require features in this version of the .NET Framework.

    3. Under the Templates node, expand the Visual C# or Visual Basic node, choose the SharePoint node, and then choose the 2010 node.

    4. Choose the SharePoint 2010 Project template, and then enter ModuleTest as the name of your project.

  4. In Solution Explorer, choose the ModuleTest project node.

    A new custom property Map Images Folder appears in the Properties window with a default value of False.

  5. Change the value of that property to True.

    An Images resource folder is added to the SharePoint project.

  6. Change the value of that property back to False.

    If you choose the Yes button in the Delete the Images folder? dialog box, the Images resource folder is deleted from the SharePoint project.

  7. Close the experimental instance of Visual Studio.

See Also

Concepts

Extending SharePoint Projects

How to: Add a Property to SharePoint Projects

Converting Between SharePoint Project System Types and Other Visual Studio Project Types

Saving Data in Extensions of the SharePoint Project System

Associating Custom Data with SharePoint Tools Extensions