Search code examples
visual-studiovisual-studio-2010envdte

Alternative to CfgPropertyPagesGuidsAddCSharp for Visual Studio 2010


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.


Solution

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