1

I am trying to develop a VSIX package that adds a tab to the project designer for a couple of custom project properties. I have the package working in VS 2013 using CfgPropertyPagesGuidsAddCSharp as described in this post. However, after porting the VSIX project to VS 2010, the custom property page is not loaded.

This question from 2011 doesn't have an answer. The answer to another question suggests creating a custom project subtype, but that seems like an awful amount of work just to be able to edit some additional project properties from the GUI. This is my first time working on a VSIX package, so I'm trying to keep things as simple as possible.

I've tried browsing through the source for the .NET project system, but I'm not sure exactly what I'd be looking for to properly register the page; any guidance would be much appreciated.

Thanks.

David Kleszyk
  • 642
  • 3
  • 10

2 Answers2

0

Sometime ago I wrote a blog post describing how to add a custom property page using the CfgPropertyPagesGuidsAddCSharp property. Perhaps you can find it useful.

Igal Tabachnik
  • 31,174
  • 15
  • 92
  • 157
0

I ended up creating a project sub-type for the package, which was easier than I expected. The bigger challenge was figuring out a way to share the application code between the VS2013 and VS2010 packages, since they reference different SDK versions. I ended up creating two separate project files, and including the shared code as a link reference in each project.

I created my own IPropertyPage implementation modeled on PropPageBase and PropPageUserControlBase. I have included a portion of that code for reference, since the Microsoft-provided code is more complicated.

Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Diagnostics.CodeAnalysis
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Imports Microsoft.VisualStudio
Imports Microsoft.VisualStudio.OLE.Interop
Imports Microsoft.VisualStudio.Shell.Interop

Imports ControlPosition = System.Drawing.Point
Imports ControlSize = System.Drawing.Size

<ComVisible(True)>
Public MustInherit Class PropertyPageProviderBase
    Implements IPropertyPage, IDisposable

    Private ReadOnly _dirtyProperties As New Dictionary(Of String, String)()

    Private _control As Control
    Private _defaultSize As System.Drawing.Size?
    Private _hostedInNative As Boolean
    Private _objects As Object()
    Private _pageSite As IPropertyPageSite

    <SuppressMessage( _
        "Microsoft.Reliability", _
        "CA2006:UseSafeHandleToEncapsulateNativeResources", _
        Justification:="Handle is not owned by us, we are just tracking a reference")>
    Private _previousParent As IntPtr

    Protected Sub New()
    End Sub

    ' ...

    Protected Property [Property](propertyName As String) As String
        Get
            If String.IsNullOrEmpty(propertyName) Then
                If propertyName Is Nothing Then
                    Throw New ArgumentNullException("propertyName")
                End If
                Throw New ArgumentException( _
                    "Empty property name is invalid", _
                    "propertyName")
            End If
            Dim dirtyValue As String = Nothing
            If _dirtyProperties.TryGetValue(propertyName, dirtyValue) Then
                Return dirtyValue
            End If
            Return ReadProperty(propertyName)
        End Get
        Set(value As String)
            If String.IsNullOrEmpty(propertyName) Then
                If propertyName Is Nothing Then
                    Throw New ArgumentNullException("propertyName")
                End If
                Throw New ArgumentException( _
                    "Empty property name is invalid", _
                    "propertyName")
            End If
            If _objects IsNot Nothing Then
                _dirtyProperties.Item(propertyName) = value
                If _pageSite IsNot Nothing Then
                    _pageSite.OnStatusChange(PROPPAGESTATUS.DIRTY)
                End If
            Else
                Debug.Fail("Accessing property while not bound to project")
            End If
        End Set
    End Property

    ' ...

    Protected Overridable Sub Apply()
        If _objects Is Nothing Then
            If _dirtyProperties.Count <> 0 Then
                Debug.Fail("Cannot save changes. Not bound to project")
            End If
            Exit Sub
        End If
        For Each dirtyProperty As KeyValuePair(Of String, String) In _dirtyProperties
            WriteProperty(dirtyProperty.Key, dirtyProperty.Value)
        Next
        _dirtyProperties.Clear()
        If _pageSite IsNot Nothing Then
            _pageSite.OnStatusChange(PROPPAGESTATUS.CLEAN)
        End If
    End Sub

    ' ...

    Private Shared Function ContainsMultipleProjects(vsObjects As Object()) As Boolean
        Debug.Assert(vsObjects IsNot Nothing)
        If vsObjects IsNot Nothing AndAlso vsObjects.Length > 1 Then
            Dim first As IVsHierarchy = GetProjectHierarchy(vsObjects(0))
            For i As Integer = 1 To vsObjects.Length - 1
                Dim current As IVsHierarchy = GetProjectHierarchy(vsObjects(i))
                If current IsNot first Then
                    Return True
                End If
            Next
        End If
        Return False
    End Function

    ' ...

    Private Shared Function GetProjectHierarchy(vsObject As Object) As IVsHierarchy
        Dim hierarchy As IVsHierarchy = Nothing
        Dim itemId As UInteger
        Dim vsCfgBrowsable As IVsCfgBrowseObject = TryCast(vsObject, IVsCfgBrowseObject)
        If vsCfgBrowsable IsNot Nothing Then
            ErrorHandler.ThrowOnFailure(vsCfgBrowsable.GetProjectItem(hierarchy, itemId))
            Return hierarchy
        End If
        Dim vsBrowsable As IVsBrowseObject = TryCast(vsObject, IVsBrowseObject)
        If vsBrowsable IsNot Nothing Then
            ErrorHandler.ThrowOnFailure(vsBrowsable.GetProjectItem(hierarchy, itemId))
            Return hierarchy
        End If
        Throw New NotSupportedException("Unsupported VS object type")
    End Function 

    ' ...

    Private Shared Sub WriteProperty(vsObject As Object, propertyName As String, propertyValue As String)
        Dim hierarchy As IVsHierarchy = GetProjectHierarchy(vsObject)
        Dim buildStorage As IVsBuildPropertyStorage = TryCast(hierarchy, IVsBuildPropertyStorage)
        If buildStorage Is Nothing Then
            Debug.Fail("Unsupported VS object")
            Exit Sub
        End If
        ErrorHandler.ThrowOnFailure(buildStorage.SetPropertyValue( _
                                    propertyName, _
                                    String.Empty, _
                                    STORAGETYPE.PROJECT_FILE, _
                                    propertyValue))
    End Sub

    ' ...

    Private Sub _SetObjects(cObjects As UInteger, ppunk() As Object) Implements IPropertyPage.SetObjects
        If cObjects = 0 OrElse ppunk Is Nothing OrElse ppunk.Length = 0 Then
            SetObjects(Nothing)
            Exit Sub
        End If
        If ContainsMultipleProjects(ppunk) Then
            SetObjects(Nothing)
            Exit Sub
        End If
        Debug.Assert(cObjects = CUInt(ppunk.Length), "Huh?")
        SetObjects(ppunk)
    End Sub

    ' ...

    Private Sub SetObjects(vsObjects As Object())
        _dirtyProperties.Clear()
        _objects = vsObjects
        OnObjectsChanged(EventArgs.Empty)
    End Sub

    ' ...

    Private Sub WriteProperty(propertyName As String, propertyValue As String)
        If _objects Is Nothing Then
            Debug.Fail("Accessing property while not bound to project")
            Exit Sub
        End If
        Debug.Assert(_objects.Length <> 0, "Should never have zero objects if collection is non-null")
        For i As Integer = 0 To _objects.Length - 1
            WriteProperty(_objects(i), propertyName, propertyValue)
        Next
    End Sub

End Class

Creating the package was fairly straightforward; just remember to call RegisterProjectFactory during the initialization step.

Imports System
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports Microsoft.VisualStudio.Modeling.Shell
Imports Microsoft.VisualStudio.Shell
Imports Microsoft.VisualStudio.Shell.Interop

<ComVisible(True)>
<ProvideBindingPath()>
<Guid(Guids.MyCustomPackage)>
<PackageRegistration( _
    UseManagedResourcesOnly:=True)>
<ProvideAutoLoad(UIContextGuids.SolutionExists)>
<ProvideProjectFactory( _
    GetType(MyCustomProjectFactory), _
    Nothing, _
    Nothing, _
    Nothing, _
    Nothing, _
    Nothing)>
<ProvideObject( _
    GetType(MyCustomPropertyPageProvider))>
Public Class MyCustomPackage
    Inherits Package
    Protected Overrides Sub Initialize()
        MyBase.Initialize()
        Dim factory As New MyCustomProjectFactory(Me)
        Try
            Me.RegisterProjectFactory(factory)
        Catch ex As ArgumentException
            Debug.Fail(ex.Message, ex.ToString())
        End Try
    End Sub
End Class

I didn't use the MPF ProjectFactory class, since the MPF isn't designed for project sub-types. Instead, I inherited directly from FlavoredProjectFactoryBase.

Imports System
Imports System.Diagnostics.CodeAnalysis
Imports System.Runtime.InteropServices
Imports Microsoft.VisualStudio.Shell.Flavor

<SuppressMessage( _
    "Microsoft.Interoperability", _
    "CA1405:ComVisibleTypeBaseTypesShouldBeComVisible", _
    Justification:="Blame Microsoft? No other way around this")>
<ComVisible(True)>
<Guid(Guids.MyCustomProjectFactory)>
Public Class MyCustomProjectFactory
    Inherits FlavoredProjectFactoryBase

    Private ReadOnly _package As MyCustomPackage

    Public Sub New()
        Me.New(Nothing)
    End Sub

    Public Sub New(package As MyCustomPackage)
        If package Is Nothing Then
            Throw New ArgumentNullException("package")
        End If
        _package = package
    End Sub

    Protected Overrides Function PreCreateForOuter(outerProjectIUnknown As IntPtr) As Object
        Return New MyCustomProject(_package)
    End Function

End Class

The project class then needs to add the GUID for the custom property page to the list of property page GUIDs.

Imports System
Imports System.Collections.Generic
Imports System.Diagnostics.CodeAnalysis
Imports System.Runtime.InteropServices
Imports Microsoft.VisualStudio
Imports Microsoft.VisualStudio.Shell.Flavor
Imports Microsoft.VisualStudio.Shell.Interop

<SuppressMessage( _
    "Microsoft.Interoperability", _
    "CA1405:ComVisibleTypeBaseTypesShouldBeComVisible", _
    Justification:="Blame Microsoft? No other way around this")>
<ComVisible(True)>
<Guid(Guids.MyCustomProject)>
Public Class MyCustomProject
    Inherits FlavoredProjectBase

    Private Const GuidFormat As String = "B"

    Private Shared ReadOnly PageSeparators As String() = {";"}

    Private ReadOnly _package As MyCustomPackage

    Public Sub New()
        Me.New(Nothing)
    End Sub

    Public Sub New(package As MyCustomPackage)
        If package Is Nothing Then
            Throw New ArgumentNullException("package")
        End If
        _package = package
    End Sub

    Protected Overrides Function GetProperty(itemId As UInteger, propId As Integer, ByRef [property] As Object) As Integer
        If propId = CInt(__VSHPROPID2.VSHPROPID_PropertyPagesCLSIDList) Then
            ErrorHandler.ThrowOnFailure(MyBase.GetProperty(itemId, propId, [property]))
            Dim pages As New HashSet(Of String)()

            If [property] IsNot Nothing Then
                For Each page As String In CStr([property]).Split(PageSeparators, StringSplitOptions.RemoveEmptyEntries)
                    Dim blah As Guid = Nothing
                    If Guid.TryParseExact(page, GuidFormat, blah) Then
                        pages.Add(page)
                    End If
                Next
            End If

            pages.Add(Guids.MyCustomPropertyPageProviderGuid.ToString(GuidFormat))
            [property] = String.Join(PageSeparators(0), pages)
            Return VSConstants.S_OK
        End If
        Return MyBase.GetProperty(itemId, propId, [property])
    End Function

    Protected Overrides Sub SetInnerProject(innerIUnknown As IntPtr)
        If MyBase.serviceProvider Is Nothing Then
            MyBase.serviceProvider = _package
        End If
        MyBase.SetInnerProject(innerIUnknown)
    End Sub

End Class

One last hint for anybody who's having trouble getting things to work: you have to open your project file in an XML editor and adjust some of the build properties manually. At a minimum you'll need to set GeneratePkgDefFile and IncludeAssemblyInVSIXContainer to true.

David Kleszyk
  • 642
  • 3
  • 10