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