Walkthrough: Creating a Site Column Project Item with a Project Template, Part 2
After you define a custom type of SharePoint project item and associate it with a project template in Visual Studio, you might also want to provide a wizard for the template. You can use the wizard to collect information from users when they use your template to create a new project that contains the project item. The information that you collect can be used to initialize the project item.
In this walkthrough, you will add a wizard to the Site Column project template that is demonstrated in Walkthrough: Creating a Site Column Project Item with a Project Template, Part 1. When a user creates a Site Column project, the wizard collects information about the site column (such as its base type and group) and adds this information to the Elements.xml file in the new project.
This walkthrough demonstrates the following tasks:
Creating a wizard for a custom SharePoint project item type that is associated with a project template.
Defining a custom wizard UI that resembles the built-in wizards for SharePoint projects in Visual Studio 2010.
Creating two SharePoint commands that are used to call into the local SharePoint site while the wizard is running. SharePoint commands are methods that can be used by Visual Studio extensions to call APIs in the SharePoint server object model. For more information, see Calling into the SharePoint Object Models.
Using replaceable parameters to initialize SharePoint project files with data that you collect in the wizard.
Creating a new .snk file in each new Site Column project instance. This file is used to sign the project output so that the SharePoint solution assembly can be deployed to the global assembly cache.
Debugging and testing the wizard.
Note
You can download a sample that contains the completed projects, code, and other files for this walkthrough from the following location: https://go.microsoft.com/fwlink/?LinkId=191369.
Prerequisites
To perform this walkthrough, you must first create the SiteColumnProjectItem solution by completing Walkthrough: Creating a Site Column Project Item with a Project Template, Part 1.
You also need the following components on the development computer to complete this walkthrough:
Supported editions of Microsoft Windows, SharePoint, and Visual Studio. For more information, see Requirements for Developing SharePoint Solutions.
The Visual Studio 2010 SDK. This walkthrough uses the VSIX Project template in the SDK to create a VSIX package to deploy the project item. For more information, see Extending the SharePoint Tools in Visual Studio.
Knowledge of the following concepts is helpful, but not required, to complete the walkthrough:
Wizards for project and item templates in Visual Studio. For more information, see How to: Use Wizards with Project Templates and the IWizard interface.
Site columns in SharePoint. For more information, see Columns.
Understanding the Wizard Components
The wizard that is demonstrated in this walkthrough contains several components. The following table describes these components.
Component |
Description |
---|---|
Wizard implementation |
This is a class, named SiteColumnProjectWizard, which implements the IWizard interface. This interface defines the methods that Visual Studio calls when the wizard starts and finishes, and at certain times while the wizard runs. |
Wizard UI |
This is a WPF-based window, named WizardWindow. This window includes two user controls, named Page1 and Page2. These user controls represent the two pages of the wizard. In this walkthrough, the RunStarted method of the wizard implementation displays the wizard UI. |
Wizard data model |
This is an intermediary class, named SiteColumnWizardModel, which provides a layer between the wizard UI and the wizard implementation. This sample uses this class to help abstract the wizard implementation and the wizard UI from each other; this class is not a required component of all wizards. In this walkthrough, the wizard implementation passes a SiteColumnWizardModel object to the wizard window when it displays the wizard UI. The wizard UI uses methods of this object to save the values of controls in the UI, and to perform tasks like verifying that the input site URL is valid. After the user finishes the wizard, the wizard implementation uses the SiteColumnWizardModel object to determine the final state of the UI. |
Project signing manager |
This is a helper class, named ProjectSigningManager, which is used by the wizard implementation to create a new key.snk file in each new project instance. |
SharePoint commands |
These are methods that are used by the wizard data model to call into the local SharePoint site while the wizard is running. Because SharePoint commands must target the .NET Framework 3.5, these commands are implemented in a different assembly than the rest of the wizard code. |
Creating the Projects
To complete this walkthrough, you need to add several projects to the SiteColumnProjectItem solution that you created in Walkthrough: Creating a Site Column Project Item with a Project Template, Part 1:
A WPF project. You will implement the IWizard interface and define the wizard UI in this project.
A class library project that defines the SharePoint commands. This project must target the.NET Framework 3.5.
Start the walkthrough by creating the projects.
To create the WPF project
Open the SiteColumnProjectItem solution in Visual Studio.
In Solution Explorer, right-click the solution node, point to Add, and then click New Project.
Note
In Visual Basic projects, the solution node appears only when the Always show solution check box is selected in the General, Projects and Solutions, Options Dialog Box.
In the Add New Project dialog box, expand the Visual C# or Visual Basic nodes, and then click Windows.
In the combo box at the top of the Add New Project dialog box, make sure that .NET Framework 4 is selected.
Select the WPF User Control Library project template.
In the Name box, type ProjectTemplateWizard.
Click OK.
Visual Studio adds the ProjectTemplateWizard project to the solution.
Delete the UserControl1.xaml file from the project.
To create the SharePoint commands project
In Solution Explorer, right-click the solution node, point to Add, and then click New Project.
In the Add New Project dialog box, expand Visual C# or Visual Basic, and then click Windows.
Click the Class Library project template.
In the combo box at the top of the dialog box, select .NET Framework 3.5.
In the Name box, type SharePointCommands.
Click OK.
Visual Studio adds the SharePointCommands project to the solution and opens the default Class1 code file.
Delete the Class1 code file from the project.
Configuring the Projects
Before you create the wizard, you have to add some code files and assembly references to the projects.
To configure the wizard project
In Solution Explorer, right-click the ProjectTemplateWizard project node and then click Properties.
In the Project Designer, change the target framework from the .NET Framework 4 Client Profile to the .NET Framework 4. In Visual C# projects, you can do this on the Application tab; for Visual Basic projects, you can do this on the Compile tab. For more information, see How to: Target a Specific .NET Framework Version or Profile.
Note
By default, when you create a new project that targets the .NET Framework 4, the project targets the client profile. This walkthrough requires the full .NET Framework 4.
In the ProjectTemplateWizard project, add a new Window (WPF) item to the project. Name the item WizardWindow.
Add two new User Control (WPF) items to the project. Name these items Page1 and Page2.
Add four code files with the following names:
SiteColumnProjectWizard
SiteColumnWizardModel
ProjectSigningManager
CommandIds
On the Project menu, click Add Reference.
On the .NET tab, press CTRL and click the following assemblies, and then click OK:
EnvDTE
Microsoft.VisualStudio.OLE.Interop
Microsoft.VisualStudio.SharePoint
Microsoft.VisualStudio.Shell.10.0
Microsoft.VisualStudio.Shell.Interop.10.0
Microsoft.VisualStudio.TemplateWizardInterface
In Solution Explorer, under the References folder for the ProjectTemplateWizard project, click EnvDTE.
Note
In Visual Basic projects, you must click the Show All Files button in Solution Explorer to see the References folder.
In the Properties window, change the Embed Interop Types property to False.
If you are developing a Visual Basic project, import the ProjectTemplateWizard namespace into your project by using the Project Designer. For more information, see How to: Add or Remove Imported Namespaces (Visual Basic).
To configure the SharePointCommands project
In the SharePointCommands project, add a code file called Commands.
In Solution Explorer, click the SharePointCommands project node.
On the Project menu, select Add Existing Item.
In the Add Existing Item dialog box, browse to the folder that contains the code files for the ProjectTemplateWizard project.
Select the CommandIds code file.
Click the drop-down menu for the Add button and select Add As Link.
Visual Studio adds the code file to the SharePointCommands project as a link. This means that the code file is located in the ProjectTemplateWizard project, but the code in the file is also compiled in the SharePointCommands project.
On the Project menu, click Add Reference.
On the .NET tab, press CTRL and select the following assemblies, and then click OK:
Microsoft.SharePoint
Microsoft.VisualStudio.SharePoint.Commands
Creating the Wizard Model, Signing Manager, and SharePoint Command IDs
Add code to the ProjectTemplateWizard project to implement the following components in the sample:
The SharePoint command IDs. These are strings that identify the SharePoint commands that are used by the wizard. Later in this walkthrough, you will add code to the SharePointCommands project to implement the commands.
The wizard data model.
The project signing manager.
For more information about these components, see Understanding the Wizard Components.
To define the SharePoint command IDs
In the ProjectTemplateWizard project, open the CommandIds code file.
Replace the entire contents of this file with the following code.
Namespace Contoso.SharePoint.Commands Public Class CommandIds Public Const GetFieldTypes As String = "Contoso.Commands.GetFieldTypes" Public Const ValidateSite As String = "Contoso.Commands.ValidateSite" End Class End Namespace
namespace Contoso.SharePoint.Commands { public static class CommandIds { public const string GetFieldTypes = "Contoso.Commands.GetFieldTypes"; public const string ValidateSite = "Contoso.Commands.ValidateSite"; } }
To create the wizard model
Open the SiteColumnWizardModel code file.
Replace the entire contents of this file with the following code.
Imports EnvDTE Imports Microsoft.VisualStudio.SharePoint Imports Microsoft.VisualStudio Imports Microsoft.VisualStudio.Shell Imports Microsoft.VisualStudio.Shell.Interop Imports IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider Public Class SiteColumnWizardModel Private dteObject As DTE Private projectServiceValue As ISharePointProjectService Private validatedUrls As New List(Of String) Friend Sub New(ByVal dteObject As DTE, ByVal requiresFarmPriveleges As Boolean) Me.dteObject = dteObject ' Initialize default values for wizard choices. IsSandboxed = Not requiresFarmPriveleges IsSecondPagePopulated = False FieldType = "Text" FieldGroup = "Custom Columns" FieldName = "My Custom Column" CurrentSiteUrl = GetLocalHostUrl() End Sub #Region "Helper methods used by the wizard UI" ' Specifies whether the current site URL is valid. Uses the ValidateSite SharePoint command to do this. Friend Function ValidateCurrentUrl(ByVal errorMessage As String) As Boolean Dim isValid As Boolean = False errorMessage = String.Empty If validatedUrls.Contains(CurrentSiteUrl) Then isValid = True Else Dim uriToValidate As Uri = New Uri(CurrentSiteUrl, UriKind.Absolute) Dim vsThreadedWaitDialog As IVsThreadedWaitDialog2 = Nothing Try vsThreadedWaitDialog = ShowProgressDialog("Connect to SharePoint", "Connecting to SharePoint site " + CurrentSiteUrl) isValid = Me.ProjectService.SharePointConnection.ExecuteCommand(Of Uri, Boolean)( Contoso.SharePoint.Commands.CommandIds.ValidateSite, uriToValidate) Catch ex As Exception errorMessage = "An error occurred while validating the site. " + ex.Message Finally If isValid Then validatedUrls.Add(CurrentSiteUrl) End If If vsThreadedWaitDialog IsNot Nothing Then CloseProgressDialog(vsThreadedWaitDialog) End If End Try End If Return isValid End Function ' Gets the available field types from the SharePoint site. Uses the GetFieldTypes SharePoint command to do this. Friend Function GetFieldTypes() As ArrayList ' If we have not yet validated this site, do it now. Dim errorMessage As String = String.Empty If Not ValidateCurrentUrl(errorMessage) Then MessageBox.Show(String.Format("Cannot connect to the SharePoint site: {0}. {1}", CurrentSiteUrl, errorMessage), "SharePoint Connection Error") Return Nothing End If ' Site is valid, so go ahead and get the available field types. Dim siteUri As Uri = New Uri(CurrentSiteUrl, UriKind.Absolute) Dim vsThreadedWaitDialog As IVsThreadedWaitDialog2 = ShowProgressDialog( "Connect to SharePoint", "Connecting to SharePoint site " + CurrentSiteUrl) Dim fieldTypesArray As String() = Me.ProjectService.SharePointConnection.ExecuteCommand(Of Uri, String())( Contoso.SharePoint.Commands.CommandIds.GetFieldTypes, siteUri) If vsThreadedWaitDialog IsNot Nothing Then CloseProgressDialog(vsThreadedWaitDialog) End If Return New ArrayList(fieldTypesArray) End Function ' Returns the default column group names in SharePoint. Friend Function GetFieldGroups() As List(Of String) Dim groups As List(Of String) = New List(Of String)() groups.Add("Base Columns") groups.Add("Core Contact and Calendar Columns") groups.Add("Core Document Columns") groups.Add("Core Task and Issue Columns") groups.Add("Extended Columns") Return groups End Function #End Region #Region "Properties shared by the wizard implementation and the wizard UI" Friend ReadOnly Property ProjectService As ISharePointProjectService Get If projectServiceValue Is Nothing Then projectServiceValue = GetProjectService() End If Return projectServiceValue End Get End Property Friend Property IsSecondPagePopulated As Boolean Friend Property IsSandboxed As Boolean Friend Property FieldType As String Friend Property FieldGroup As String Friend Property FieldName As String Friend Property CurrentSiteUrl As String #End Region #Region "Private methods" Private Function GetLocalHostUrl() As String Const HttpScheme As String = "http" Dim builder As UriBuilder = New UriBuilder(HttpScheme, Environment.MachineName.ToLowerInvariant()) Return builder.ToString() End Function Private Function GetProjectService() As ISharePointProjectService Dim serviceProvider As ServiceProvider = New ServiceProvider(CType(dteObject, IOleServiceProvider)) Return CType(serviceProvider.GetService(GetType(ISharePointProjectService)), ISharePointProjectService) End Function Private Function ShowProgressDialog(ByVal caption As String, ByVal message As String) As IVsThreadedWaitDialog2 Dim oleServiceProvider As IOleServiceProvider = CType(dteObject, IOleServiceProvider) Dim dialogFactory As IVsThreadedWaitDialogFactory = CType(New ServiceProvider(oleServiceProvider).GetService( GetType(SVsThreadedWaitDialogFactory)), IVsThreadedWaitDialogFactory) If dialogFactory Is Nothing Then Throw New InvalidOperationException("The IVsThreadedWaitDialogFactory object could not be retrieved.") End If Dim vsThreadedWaitDialog As IVsThreadedWaitDialog2 = Nothing ErrorHandler.ThrowOnFailure(dialogFactory.CreateInstance(vsThreadedWaitDialog)) ErrorHandler.ThrowOnFailure(vsThreadedWaitDialog.StartWaitDialog(caption, message, Nothing, Nothing, String.Empty, 0, False, True)) Return vsThreadedWaitDialog End Function Private Sub CloseProgressDialog(ByVal vsThreadedWaitDialog As IVsThreadedWaitDialog2) If vsThreadedWaitDialog Is Nothing Then Throw New ArgumentNullException("vsThreadedWaitDialog") End If Dim canceled As Integer ErrorHandler.ThrowOnFailure(vsThreadedWaitDialog.EndWaitDialog(canceled)) End Sub #End Region End Class
using System; using System.Collections; using System.Collections.Generic; using System.Windows; using EnvDTE; using Microsoft.VisualStudio.SharePoint; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; namespace ProjectTemplateWizard { internal class SiteColumnWizardModel { private DTE dteObject; private ISharePointProjectService projectServiceValue; private List<string> validatedUrls = new List<string>(); internal SiteColumnWizardModel(DTE dteObject, bool requiresFarmPriveleges) { this.dteObject = dteObject; // Initialize default values for wizard choices. IsSandboxed = !requiresFarmPriveleges; IsSecondPagePopulated = false; FieldType = "Text"; FieldGroup = "Custom Columns"; FieldName = "My Custom Column"; CurrentSiteUrl = GetLocalHostUrl(); } #region Helper methods used by the wizard UI // Specifies whether the current site URL is valid. Uses the ValidateSite SharePoint command to do this. internal bool ValidateCurrentUrl(out string errorMessage) { bool isValid = false; errorMessage = String.Empty; if (validatedUrls.Contains(CurrentSiteUrl)) { isValid = true; } else { Uri uriToValidate = new Uri(CurrentSiteUrl, UriKind.Absolute); IVsThreadedWaitDialog2 vsThreadedWaitDialog = null; try { vsThreadedWaitDialog = ShowProgressDialog("Connect to SharePoint", "Connecting to SharePoint site " + CurrentSiteUrl); isValid = this.ProjectService.SharePointConnection.ExecuteCommand<Uri, bool>( Contoso.SharePoint.Commands.CommandIds.ValidateSite, uriToValidate); } catch (Exception ex) { errorMessage = "An error occurred while validating the site. " + ex.Message; } finally { if (isValid) { validatedUrls.Add(CurrentSiteUrl); } if (vsThreadedWaitDialog != null) { CloseProgressDialog(vsThreadedWaitDialog); } } } return isValid; } // Gets the available field types from the SharePoint site. Uses the GetFieldTypes SharePoint command to do this. internal ArrayList GetFieldTypes() { // If we have not yet validated this site, do it now. string errorMessage; if (!ValidateCurrentUrl(out errorMessage)) { MessageBox.Show(String.Format("Cannot connect to the SharePoint site: {0}. {1}", CurrentSiteUrl, errorMessage), "SharePoint Connection Error"); return null; } // Site is valid, so go ahead and get the available field types. Uri siteUri = new Uri(CurrentSiteUrl, UriKind.Absolute); IVsThreadedWaitDialog2 vsThreadedWaitDialog = ShowProgressDialog( "Connect to SharePoint", "Connecting to SharePoint site " + CurrentSiteUrl); string[] fieldTypesArray = this.ProjectService.SharePointConnection.ExecuteCommand<Uri, string[]>( Contoso.SharePoint.Commands.CommandIds.GetFieldTypes, siteUri); if (vsThreadedWaitDialog != null) { CloseProgressDialog(vsThreadedWaitDialog); } return new ArrayList(fieldTypesArray); } // Returns the default column group names in SharePoint. internal List<string> GetFieldGroups() { List<string> groups = new List<string>(); groups.Add("Base Columns"); groups.Add("Core Contact and Calendar Columns"); groups.Add("Core Document Columns"); groups.Add("Core Task and Issue Columns"); groups.Add("Extended Columns"); return groups; } #endregion #region Properties shared by the wizard implementation and the wizard UI internal ISharePointProjectService ProjectService { get { if (projectServiceValue == null) { projectServiceValue = GetProjectService(); } return projectServiceValue; } } internal bool IsSecondPagePopulated { get; set; } internal bool IsSandboxed { get; set; } internal string FieldType { get; set; } internal string FieldGroup { get; set; } internal string FieldName { get; set; } internal string CurrentSiteUrl { get; set; } #endregion #region Private methods private string GetLocalHostUrl() { const string HttpScheme = "http"; UriBuilder builder = new UriBuilder(HttpScheme, Environment.MachineName.ToLowerInvariant()); return builder.ToString(); } private ISharePointProjectService GetProjectService() { ServiceProvider serviceProvider = new ServiceProvider(dteObject as IOleServiceProvider); return serviceProvider.GetService(typeof(ISharePointProjectService)) as ISharePointProjectService; } private IVsThreadedWaitDialog2 ShowProgressDialog(string caption, string message) { IOleServiceProvider oleServiceProvider = dteObject as IOleServiceProvider; IVsThreadedWaitDialogFactory dialogFactory = new ServiceProvider(oleServiceProvider).GetService( typeof(SVsThreadedWaitDialogFactory)) as IVsThreadedWaitDialogFactory; if (dialogFactory == null) { throw new InvalidOperationException("The IVsThreadedWaitDialogFactory object could not be retrieved."); } IVsThreadedWaitDialog2 vsThreadedWaitDialog = null; ErrorHandler.ThrowOnFailure(dialogFactory.CreateInstance(out vsThreadedWaitDialog)); ErrorHandler.ThrowOnFailure(vsThreadedWaitDialog.StartWaitDialog(caption, message, null, null, String.Empty, 0, false, true)); return vsThreadedWaitDialog; } private void CloseProgressDialog(IVsThreadedWaitDialog2 vsThreadedWaitDialog) { if (vsThreadedWaitDialog == null) { throw new ArgumentNullException("vsThreadedWaitDialog"); } int canceled; ErrorHandler.ThrowOnFailure(vsThreadedWaitDialog.EndWaitDialog(out canceled)); } #endregion } }
To create the project signing manager
Open the ProjectSigningManager code file.
Replace the entire contents of this file with the following code.
Imports EnvDTE Imports System Imports System.IO Imports System.Runtime.InteropServices Friend Class ProjectSigningManager Private Const KEY_FILENAME As String = "key.snk" Private keyBuffer As Byte() Private strongNameGenerated As Boolean = False #Region "Methods used by the project wizard" Friend Sub GenerateKeyFile() If Not strongNameGenerated Then keyBuffer = CreateNewKeyPair() strongNameGenerated = True End If End Sub Friend Sub AddKeyFile(ByVal project As Project) If strongNameGenerated Then AddKeyFileToProject(project) End If End Sub #End Region #Region "Private members" Private Function CreateNewKeyPair() As Byte() Dim buffer As IntPtr = IntPtr.Zero Dim bufferSize As UInteger Dim keyBuffer As Byte() Try If 0 = NativeMethods.StrongNameKeyGen(IntPtr.Zero, 0, buffer, bufferSize) Then Marshal.ThrowExceptionForHR(NativeMethods.StrongNameErrorInfo()) End If If buffer = IntPtr.Zero Then Throw New InvalidOperationException("Cannot generate the strong name key.") End If ' Copy generated key to managed memory. keyBuffer = New Byte(bufferSize) {} Marshal.Copy(buffer, keyBuffer, 0, CInt(bufferSize)) Finally ' Free native resources. NativeMethods.StrongNameFreeBuffer(buffer) End Try Return keyBuffer End Function Private Sub AddKeyFileToProject(ByVal project As Project) ' Save the key to a file. If keyBuffer IsNot Nothing Then Try Dim destinationDirectory As String = Path.GetDirectoryName(project.FullName) Dim keySavePath As String = Path.Combine(destinationDirectory, KEY_FILENAME) File.WriteAllBytes(keySavePath, keyBuffer) project.ProjectItems.AddFromFile(keySavePath) ' Add properties in the project to use the key for signing. Dim projProps As EnvDTE.Properties = project.Properties projProps.Item("SignAssembly").Value = True projProps.Item("AssemblyOriginatorKeyFile").Value = KEY_FILENAME Catch e As Exception Throw New Exception("Cannot add the strong name key to the project. " & e.Message, e) End Try End If End Sub Private Class NativeMethods <DllImport("mscoree.dll")> Friend Shared Function StrongNameFreeBuffer(ByVal pbMemory As IntPtr) As Integer End Function <DllImport("mscoree.dll", CharSet:=CharSet.Unicode, ExactSpelling:=True)> Friend Shared Function StrongNameKeyGen(ByVal wszKeyContainer As IntPtr, ByVal dwFlags As UInteger, _ ByRef KeyBlob As IntPtr, ByRef KeyBlobSize As UInteger) As Integer End Function <DllImport("mscoree.dll", CharSet:=CharSet.Unicode)> Friend Shared Function StrongNameErrorInfo() As Integer End Function End Class #End Region End Class
using EnvDTE; using System; using System.IO; using System.Runtime.InteropServices; namespace ProjectTemplateWizard { internal class ProjectSigningManager { private const string KEY_FILENAME = "key.snk"; private byte[] keyBuffer; private bool strongNameGenerated = false; #region Methods used by the project wizard internal void GenerateKeyFile() { if (!strongNameGenerated) { keyBuffer = CreateNewKeyPair(); strongNameGenerated = true; } } internal void AddKeyFile(Project project) { if (strongNameGenerated) { AddKeyFileToProject(project); } } #endregion #region Private members private byte[] CreateNewKeyPair() { IntPtr buffer = IntPtr.Zero; uint bufferSize; byte[] keyBuffer; try { if (0 == NativeMethods.StrongNameKeyGen(IntPtr.Zero, 0, out buffer, out bufferSize)) { Marshal.ThrowExceptionForHR(NativeMethods.StrongNameErrorInfo()); } if (buffer == IntPtr.Zero) { throw new InvalidOperationException("Cannot generate the strong name key."); } // Copy generated key to managed memory. keyBuffer = new byte[bufferSize]; Marshal.Copy(buffer, keyBuffer, 0, (int)bufferSize); } finally { // Free native resources. NativeMethods.StrongNameFreeBuffer(buffer); } return keyBuffer; } private void AddKeyFileToProject(Project project) { // Save the key to a file. if (keyBuffer != null) { try { string destinationDirectory = Path.GetDirectoryName(project.FullName); string keySavePath = Path.Combine(destinationDirectory, KEY_FILENAME); File.WriteAllBytes(keySavePath, keyBuffer); project.ProjectItems.AddFromFile(keySavePath); // Add properties in the project to use the key for signing. EnvDTE.Properties projProps = project.Properties; projProps.Item("SignAssembly").Value = true; projProps.Item("AssemblyOriginatorKeyFile").Value = KEY_FILENAME; } catch (Exception e) { throw new Exception("Cannot add the strong name key to the project. " + e.Message, e); } } } private static class NativeMethods { [DllImport("mscoree.dll")] internal extern static int StrongNameFreeBuffer(IntPtr pbMemory); [DllImport("mscoree.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] internal static extern int StrongNameKeyGen(IntPtr wszKeyContainer, uint dwFlags, out IntPtr KeyBlob, out uint KeyBlobSize); [DllImport("mscoree.dll", CharSet = CharSet.Unicode)] internal static extern int StrongNameErrorInfo(); } #endregion } }
Creating the Wizard UI
Add XAML to define the UI of the wizard window and the two user controls that provide the UI for the wizard pages, and add code to define the behavior of the window and user controls. The wizard that you create resembles the built-in wizard for SharePoint projects in Visual Studio 2010.
Note
In the following steps, your project will have some compile errors after you add XAML or code to your project. These errors will go away when you add code in later steps.
To create the wizard window UI
In the ProjectTemplateWizard project, double-click the WizardWindow.xaml file to open the window in the designer.
In the XAML view of the designer, replace the current XAML with the following XAML. The XAML defines a UI that includes a heading, a Grid that contains the wizard pages, and navigation buttons at the bottom of the window.
<ui:DialogWindow x:Class="ProjectTemplateWizard.WizardWindow" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.10.0" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Title="SharePoint Customization Wizard" Height="500" Width="700" ResizeMode="NoResize" Loaded="Window_Loaded" TextOptions.TextFormattingMode="Display"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="75*" /> <RowDefinition Height="364*" /> <RowDefinition Height="1*" /> <RowDefinition Height="60*" /> </Grid.RowDefinitions> <Grid Grid.Row="0" Name="headingGrid" Background="White"> <Label Height="28" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="18,0,0,0" Name="headingLabel" FontWeight="ExtraBold" /> </Grid> <Grid Grid.Row="1" Name="pageGrid" /> <Rectangle Grid.Row="2" Name="separatorRectangle" Fill="White" /> <StackPanel Grid.Row="3" Name="navigationPanel" Orientation="Horizontal"> <Button Content="< _Previous" Margin="300,0,0,0" Height="25" Name="previousButton" Width="85" IsEnabled="False" Click="previousButton_Click" /> <Button Content="_Next >" Margin="10,0,0,0" Height="25" Name="nextButton" Width="85" Click="nextButton_Click" IsDefault="True" /> <Button Content="_Finish" Margin="10,0,0,0" Height="25" Name="finishButton" Width="85" Click="finishButton_Click" /> <Button Content="Cancel" Margin="10,0,0,0" Height="25" Name="cancelButton" Width="85" IsCancel="True" /> </StackPanel> </Grid> </ui:DialogWindow>
Note
The window that is created in this XAML is derived from the DialogWindow base class. When you add a custom WPF dialog box to Visual Studio, it is recommended that you derive your dialog box from this class to have consistent styling with other Visual Studio dialog boxes and to avoid modal dialog issues that might otherwise occur. For more information, see How to: Create and Manage Dialog Boxes.
If you are developing a Visual Basic project, remove the ProjectTemplateWizard namespace from the WizardWindow class name in the x:Class attribute of the Window element. This is in the first line of the XAML. When you are done, the first line should look like the following.
<Window x:Class="WizardWindow"
Open the code-behind file for the WizardWindow.xaml file.
Replace the entire contents of this file with the following code.
Public Class WizardWindow Private firstPage As Page1 Private secondPage As Page2 Private Const firstPageLabel As String = "Specify the site and security level for debugging" Private Const secondPageLabel As String = "Configure the site column" Friend Sub New(ByVal presentationModel As SiteColumnWizardModel) InitializeComponent() Me.PresentationModel = presentationModel firstPage = New Page1(Me) secondPage = New Page2(Me) secondPage.Visibility = Visibility.Hidden End Sub Friend Property PresentationModel As SiteColumnWizardModel Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) headingLabel.Content = firstPageLabel pageGrid.Children.Add(firstPage) pageGrid.Children.Add(secondPage) End Sub Private Sub nextButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs) ' Initialize the second wizard page if this is the first time ' it has been shown with the current site URL. If Not PresentationModel.IsSecondPagePopulated Then If Not ValidateUrl() Then Return End If ' Refresh the UI in the second page. secondPage.ClearControls() secondPage.PopulateSiteColumnOptions() ' Do not do this work again until the user changes the site URL. PresentationModel.IsSecondPagePopulated = True End If ' Display the second wizard page and update related controls. firstPage.Visibility = Visibility.Hidden secondPage.Visibility = Visibility.Visible previousButton.IsEnabled = True nextButton.IsEnabled = False nextButton.IsDefault = False finishButton.IsDefault = True headingLabel.Content = secondPageLabel End Sub ' Display the first wizard page again and update related controls. Private Sub previousButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs) secondPage.Visibility = Visibility.Hidden firstPage.Visibility = Visibility.Visible previousButton.IsEnabled = False finishButton.IsDefault = False nextButton.IsEnabled = True nextButton.IsDefault = True headingLabel.Content = firstPageLabel End Sub Private Sub finishButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs) If ValidateUrl() Then DialogResult = True Close() End If End Sub Private Function ValidateUrl() As Boolean Dim errorMessage As String = String.Empty If Not PresentationModel.ValidateCurrentUrl(errorMessage) Then MessageBox.Show(String.Format("Cannot connect to the SharePoint site: {0}. {1}", PresentationModel.CurrentSiteUrl, errorMessage), "SharePoint Connection Error") Return False End If Return True End Function End Class
using System; using System.Windows; using Microsoft.VisualStudio.PlatformUI; namespace ProjectTemplateWizard { public partial class WizardWindow : DialogWindow { private Page1 firstPage; private Page2 secondPage; private const string firstPageLabel = "Specify the site and security level for debugging"; private const string secondPageLabel = "Configure the site column"; internal WizardWindow(SiteColumnWizardModel presentationModel) { InitializeComponent(); this.PresentationModel = presentationModel; firstPage = new Page1(this); secondPage = new Page2(this); secondPage.Visibility = Visibility.Hidden; } internal SiteColumnWizardModel PresentationModel { get; set; } private void Window_Loaded(object sender, RoutedEventArgs e) { headingLabel.Content = firstPageLabel; pageGrid.Children.Add(firstPage); pageGrid.Children.Add(secondPage); } private void nextButton_Click(object sender, RoutedEventArgs e) { // Initialize the second wizard page if this is the first time // it has been shown with the current site URL. if (!PresentationModel.IsSecondPagePopulated) { if (!ValidateUrl()) { return; } // Refresh the UI in the second page. secondPage.ClearControls(); secondPage.PopulateSiteColumnOptions(); // Do not do this work again until the user changes the site URL. PresentationModel.IsSecondPagePopulated = true; } // Display the second wizard page and update related controls. firstPage.Visibility = Visibility.Hidden; secondPage.Visibility = Visibility.Visible; previousButton.IsEnabled = true; nextButton.IsEnabled = false; finishButton.IsDefault = true; headingLabel.Content = secondPageLabel; } // Display the first wizard page again and update related controls. private void previousButton_Click(object sender, RoutedEventArgs e) { secondPage.Visibility = Visibility.Hidden; firstPage.Visibility = Visibility.Visible; previousButton.IsEnabled = false; finishButton.IsDefault = false; nextButton.IsEnabled = true; nextButton.IsDefault = true; headingLabel.Content = firstPageLabel; } private void finishButton_Click(object sender, RoutedEventArgs e) { if (ValidateUrl()) { DialogResult = true; Close(); } } private bool ValidateUrl() { string errorMessage; if (!PresentationModel.ValidateCurrentUrl(out errorMessage)) { MessageBox.Show(String.Format("Cannot connect to the SharePoint site: {0}. {1}", PresentationModel.CurrentSiteUrl, errorMessage), "SharePoint Connection Error"); return false; } return true; } } }
To create the first wizard page UI
Double-click the Page1.xaml file to open the user control in the designer.
In the XAML view of the designer, replace the current XAML with the following XAML. The XAML defines a UI that includes a text box where users can type the URL of the local site they want to use for debugging and radio buttons for specifying whether the project is sandboxed.
<UserControl x:Class="ProjectTemplateWizard.Page1" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="364" d:DesignWidth="700" Loaded="UserControl_Loaded"> <Grid Height="364" HorizontalAlignment="Left" Name="page1Grid" Width="700"> <Grid.ColumnDefinitions> <ColumnDefinition Width="20*" /> <ColumnDefinition Width="548*" /> <ColumnDefinition Width="132*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBox Grid.Row="1" Grid.Column="1" Margin="5,0,1,0" Height="23" Name="siteUrlTextBox" TextChanged="siteUrlTextBox_TextChanged" /> <Label Grid.Row="0" Grid.Column="1" Margin="0,20,0,0" Name="siteLabel" FontWeight="Bold" Target="{Binding ElementName=siteUrlTextBox}" Content="What local _site do you want to use for debugging?" /> <Button Grid.Row="1" Grid.Column="2" Content="_Validate" Height="25" Name="validateButton" Width="88" Click="validateButton_Click" HorizontalAlignment="Left" Margin="5,0,0,0" VerticalAlignment="Top" /> <Label Grid.Row="2" Grid.Column="1" Margin="0,10,0,0" Content="What is the trust level for this SharePoint solution?" Name="trustLabel" FontWeight="Bold" /> <StackPanel Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Vertical"> <RadioButton Content="Deploy as a sand_boxed solution" Margin="5,0,0,0" Name="sandboxedSolutionRadioButton" FontWeight="Bold" Checked="sandboxedSolutionRadioButton_Checked" /> <TextBlock TextWrapping="WrapWithOverflow" Margin="20,7,50,0">Clicking this option causes the solution to be deployed as a Sandboxed solution. Sandboxed solutions can be deployed by the site collection owner and are run in a secure, monitored process that has limited resource access.</TextBlock> <RadioButton Content="Deploy as a _farm solution" Margin="5,7,0,0" Name="farmSolutionRadioButton" FontWeight="Bold" Checked="farmSolutionRadioButton_Checked" /> <TextBlock TextWrapping="WrapWithOverflow" Margin="20,7,50,0">Clicking this option means that users must have SharePoint administrator privileges to run or deploy the solution.</TextBlock> </StackPanel> </Grid> </UserControl>
If you are developing a Visual Basic project, remove the ProjectTemplateWizard namespace from the Page1 class name in the x:Class attribute of the UserControl element. This is in the first line of the XAML. When you are done, the first line should look like the following.
<UserControl x:Class="Page1"
Open the code-behind file for the Page1.xaml file.
Replace the entire contents of this file with the following code.
Public Class Page1 Private mainWindow As WizardWindow Friend Sub New(ByVal mainWindow As WizardWindow) Me.mainWindow = mainWindow InitializeComponent() End Sub Private Sub UserControl_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) If (mainWindow.PresentationModel.IsSandboxed) Then sandboxedSolutionRadioButton.IsChecked = True Else sandboxedSolutionRadioButton.IsEnabled = False farmSolutionRadioButton.IsChecked = True End If siteUrlTextBox.Text = mainWindow.PresentationModel.CurrentSiteUrl End Sub ' Validate that the URL exists on the development computer. Private Sub validateButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs) Dim errorMessage As String = String.Empty validateButton.IsEnabled = False If Not mainWindow.PresentationModel.ValidateCurrentUrl(errorMessage) Then MessageBox.Show(String.Format("Cannot connect to the SharePoint site: {0}. {1}", mainWindow.PresentationModel.CurrentSiteUrl, errorMessage), "SharePoint Connection Error") Else MessageBox.Show("Successfully connected to SharePoint site " + mainWindow.PresentationModel.CurrentSiteUrl, "Connection Successful") End If validateButton.IsEnabled = True End Sub ' Prevent users from finishing the wizard if the URL is not formatted correctly. Private Sub siteUrlTextBox_TextChanged(ByVal sender As Object, ByVal e As TextChangedEventArgs) Dim url As String = EnsureTrailingSlash(siteUrlTextBox.Text) ' Perform some basic error-checking on the URL here. If url.Length > 0 AndAlso Uri.IsWellFormedUriString(Uri.EscapeUriString(url), UriKind.Absolute) Then mainWindow.finishButton.IsEnabled = True mainWindow.nextButton.IsEnabled = True validateButton.IsEnabled = True mainWindow.PresentationModel.CurrentSiteUrl = url mainWindow.PresentationModel.IsSecondPagePopulated = False Else mainWindow.finishButton.IsEnabled = False mainWindow.nextButton.IsEnabled = False validateButton.IsEnabled = False End If End Sub Private Sub sandboxedSolutionRadioButton_Checked(ByVal sender As Object, ByVal e As RoutedEventArgs) mainWindow.PresentationModel.IsSandboxed = CBool(sandboxedSolutionRadioButton.IsChecked) End Sub Private Sub farmSolutionRadioButton_Checked(ByVal sender As Object, ByVal e As RoutedEventArgs) mainWindow.PresentationModel.IsSandboxed = CBool(sandboxedSolutionRadioButton.IsChecked) End Sub Private Function EnsureTrailingSlash(ByVal url As String) If Not String.IsNullOrEmpty(url) AndAlso url(url.Length - 1) <> "/" Then url += "/" End If Return url End Function End Class
using System; using System.Windows; using System.Windows.Controls; namespace ProjectTemplateWizard { public partial class Page1 : UserControl { private WizardWindow mainWindow; internal Page1(WizardWindow mainWindow) { this.mainWindow = mainWindow; InitializeComponent(); } private void UserControl_Loaded(object sender, RoutedEventArgs e) { if (mainWindow.PresentationModel.IsSandboxed) { sandboxedSolutionRadioButton.IsChecked = true; } else { sandboxedSolutionRadioButton.IsEnabled = false; farmSolutionRadioButton.IsChecked = true; } siteUrlTextBox.Text = mainWindow.PresentationModel.CurrentSiteUrl; } // Validate that the URL exists on the development computer. private void validateButton_Click(object sender, RoutedEventArgs e) { string errorMessage; validateButton.IsEnabled = false; if (!mainWindow.PresentationModel.ValidateCurrentUrl(out errorMessage)) { MessageBox.Show(String.Format("Cannot connect to the SharePoint site: {0}. {1}", mainWindow.PresentationModel.CurrentSiteUrl, errorMessage), "SharePoint Connection Error"); } else { MessageBox.Show("Successfully connected to SharePoint site " + mainWindow.PresentationModel.CurrentSiteUrl, "Connection Successful"); } validateButton.IsEnabled = true; } // Prevent users from finishing the wizard if the URL is not formatted correctly. private void siteUrlTextBox_TextChanged(object sender, TextChangedEventArgs e) { string url = EnsureTrailingSlash(siteUrlTextBox.Text); // Perform some basic error-checking on the URL here. if ((url.Length > 0) && (Uri.IsWellFormedUriString(Uri.EscapeUriString(url), UriKind.Absolute))) { mainWindow.finishButton.IsEnabled = true; mainWindow.nextButton.IsEnabled = true; validateButton.IsEnabled = true; mainWindow.PresentationModel.CurrentSiteUrl = url; mainWindow.PresentationModel.IsSecondPagePopulated = false; } else { mainWindow.finishButton.IsEnabled = false; mainWindow.nextButton.IsEnabled = false; validateButton.IsEnabled = false; } } private void sandboxedSolutionRadioButton_Checked(object sender, RoutedEventArgs e) { mainWindow.PresentationModel.IsSandboxed = (bool)sandboxedSolutionRadioButton.IsChecked; } private void farmSolutionRadioButton_Checked(object sender, RoutedEventArgs e) { mainWindow.PresentationModel.IsSandboxed = (bool)sandboxedSolutionRadioButton.IsChecked; } private string EnsureTrailingSlash(string url) { if (!String.IsNullOrEmpty(url) && url[url.Length - 1] != '/') { url += '/'; } return url; } } }
To create the second wizard page UI
Double-click the Page2.xaml file to open the user control in the designer.
In the XAML view of the designer, replace the current XAML with the following XAML. The XAML defines a UI that includes a drop-down list for choosing the base type of the site column, a combo box for specifying a built-in or custom group under which to display the site column in the gallery, and a text box for specifying the name of the site column.
<UserControl x:Class="ProjectTemplateWizard.Page2" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="364" d:DesignWidth="700" Loaded="UserControl_Loaded"> <Grid Height="364" HorizontalAlignment="Left" Name="page2Grid" Width="700"> <Grid.ColumnDefinitions> <ColumnDefinition Width="20*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="450*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Label Grid.Row="0" Grid.Column="1" Margin="0,20,0,0" Content="_Type:" Name="fieldTypeLabel" FontWeight="Bold" Target="{Binding ElementName=fieldTypeComboBox}"/> <Label Grid.Row="1" Grid.Column="1" Margin="0,10,0,0" Content="_Group:" Name="groupLabel" FontWeight="Bold" Target="{Binding ElementName=groupComboBox}"/> <Label Grid.Row="2" Grid.Column="1" Margin="0,10,0,0" Content="_Name:" Name="nameLabel" FontWeight="Bold" Target="{Binding ElementName=nameTextBox}"/> <ComboBox Grid.Row="0" Grid.Column="2" HorizontalAlignment="Left" Margin="0,20,0,0" Height="23" Name="fieldTypeComboBox" Width="450" SelectionChanged="fieldTypeComboBox_SelectionChanged" /> <ComboBox Grid.Row="1" Grid.Column="2" HorizontalAlignment="Left" Margin="0,10,0,0" Height="23" Name="groupComboBox" Width="450" IsEditable="True" /> <TextBox Grid.Row="2" Grid.Column="2" HorizontalAlignment="Left" Margin="0,10,0,0" Height="23" Name="nameTextBox" Width="450" TextChanged="nameTextBox_TextChanged" /> </Grid> </UserControl>
If you are developing a Visual Basic project, remove the ProjectTemplateWizard namespace from the Page2 class name in the x:Class attribute of the UserControl element. This is in the first line of the XAML. When you are done, the first line should look like the following.
<UserControl x:Class="Page2"
Open the code-behind file for the Page2.xaml file.
Replace the entire contents of this file with the following code.
Public Class Page2 Private mainWindow As WizardWindow Private innerTextBoxForGroupComboBox As TextBox Friend Sub New(ByVal mainWindow As WizardWindow) Me.mainWindow = mainWindow InitializeComponent() End Sub Friend Sub ClearControls() fieldTypeComboBox.Items.Clear() groupComboBox.Items.Clear() nameTextBox.Clear() End Sub Friend Sub PopulateSiteColumnOptions() ' Add the available field type names to the combo box. Dim fieldTypes As System.Collections.ArrayList = mainWindow.PresentationModel.GetFieldTypes() If fieldTypes IsNot Nothing Then fieldTypes.Sort() For Each fieldValue As String In fieldTypes fieldTypeComboBox.Items.Add(fieldValue) Next fieldTypeComboBox.SelectedIndex = 0 End If ' Add the default group names to the combo box. Dim fieldGroups As List(Of String) = mainWindow.PresentationModel.GetFieldGroups() For Each fieldGroupValue As String In fieldGroups groupComboBox.Items.Add(fieldGroupValue) Next groupComboBox.SelectedIndex = 0 End Sub Private Sub UserControl_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) ' Handle the TextChanged event of the underlying TextBox for the ComboBox. This enables us to determine ' 1) when the user selects an item in the list and 2) when they type their own custom group name. ' The ComboBox.SelectionChanged event is not raised when you type in an editable ComboboBox. innerTextBoxForGroupComboBox = CType(groupComboBox.Template.FindName( "PART_EditableTextBox", groupComboBox), TextBox) AddHandler innerTextBoxForGroupComboBox.TextChanged, AddressOf innerTextBoxForGroupComboBox_TextChanged End Sub Private Sub fieldTypeComboBox_SelectionChanged(ByVal sender As Object, ByVal e As SelectionChangedEventArgs) mainWindow.PresentationModel.FieldType = CStr(fieldTypeComboBox.SelectedItem) End Sub Private Sub innerTextBoxForGroupComboBox_TextChanged(ByVal sender As Object, ByVal e As TextChangedEventArgs) mainWindow.PresentationModel.FieldGroup = groupComboBox.Text End Sub Private Sub nameTextBox_TextChanged(ByVal sender As Object, ByVal e As TextChangedEventArgs) mainWindow.PresentationModel.FieldName = nameTextBox.Text End Sub End Class
using System.Windows; using System.Windows.Controls; namespace ProjectTemplateWizard { public partial class Page2 : UserControl { private WizardWindow mainWindow; private TextBox innerTextBoxForGroupComboBox; internal Page2(WizardWindow mainWindow) { this.mainWindow = mainWindow; InitializeComponent(); } internal void ClearControls() { fieldTypeComboBox.Items.Clear(); groupComboBox.Items.Clear(); nameTextBox.Clear(); } internal void PopulateSiteColumnOptions() { // Add the available field type names to the combo box. System.Collections.ArrayList fieldTypes = mainWindow.PresentationModel.GetFieldTypes(); if (fieldTypes != null) { fieldTypes.Sort(); foreach (string fieldValue in fieldTypes) { fieldTypeComboBox.Items.Add(fieldValue); } fieldTypeComboBox.SelectedIndex = 0; } // Add the default group names to the combo box. System.Collections.Generic.List<string> fieldGroups = mainWindow.PresentationModel.GetFieldGroups(); foreach (string fieldGroupValue in fieldGroups) { groupComboBox.Items.Add(fieldGroupValue); } groupComboBox.SelectedIndex = 0; } private void UserControl_Loaded(object sender, RoutedEventArgs e) { // Handle the TextChanged event of the underlying TextBox for the ComboBox. This enables us to determine // 1) when the user selects an item in the list and 2) when they type their own custom group name. // The ComboBox.SelectionChanged event is not raised when you type in an editable ComboboBox. innerTextBoxForGroupComboBox = groupComboBox.Template.FindName( "PART_EditableTextBox", groupComboBox) as TextBox; innerTextBoxForGroupComboBox.TextChanged += innerTextBoxForGroupComboBox_TextChanged; } private void fieldTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { mainWindow.PresentationModel.FieldType = (string)fieldTypeComboBox.SelectedItem; } void innerTextBoxForGroupComboBox_TextChanged(object sender, TextChangedEventArgs e) { mainWindow.PresentationModel.FieldGroup = groupComboBox.Text; } private void nameTextBox_TextChanged(object sender, TextChangedEventArgs e) { mainWindow.PresentationModel.FieldName = nameTextBox.Text; } } }
Implementing the Wizard
Define the main functionality of the wizard by implementing the IWizard interface. This interface defines the methods that Visual Studio calls when the wizard starts and finishes, and at certain times while the wizard runs.
To implement the wizard
In the ProjectTemplateWizard project, open the SiteColumnProjectWizard code file.
Replace the entire contents of this file with the following code.
Imports EnvDTE Imports Microsoft.VisualStudio.SharePoint Imports Microsoft.VisualStudio.TemplateWizard Imports System Imports System.Collections.Generic Public Class SiteColumnProjectWizard Implements IWizard Private wizardUI As WizardWindow Private dteObject As DTE Private presentationModel As SiteColumnWizardModel Private signingManager As ProjectSigningManager Public Sub New() signingManager = New ProjectSigningManager() End Sub Public Sub RunStarted(ByVal automationObject As Object, ByVal replacementsDictionary As Dictionary(Of String, String), _ ByVal runKind As WizardRunKind, ByVal customParams() As Object) Implements IWizard.RunStarted dteObject = CType(automationObject, DTE) presentationModel = New SiteColumnWizardModel(dteObject, False) If Not presentationModel.ProjectService.IsSharePointInstalled Then Dim errorString As String = "A SharePoint server is not installed on this computer. A SharePoint server " & "must be installed to work with SharePoint projects." System.Windows.MessageBox.Show(errorString, "SharePoint Not Installed", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error) Throw New WizardCancelledException(errorString) End If wizardUI = New WizardWindow(presentationModel) Dim dialogCompleted? As Boolean = wizardUI.ShowModal() If (dialogCompleted = True) Then replacementsDictionary.Add("$selectedfieldtype$", presentationModel.FieldType) replacementsDictionary.Add("$selectedgrouptype$", presentationModel.FieldGroup) replacementsDictionary.Add("$fieldname$", presentationModel.FieldName) signingManager.GenerateKeyFile() Else Throw New WizardCancelledException() End If End Sub ' Populate the SiteUrl and IsSandboxedSolution properties in the new project, and add a new ' key.snk file to the project. Public Sub ProjectFinishedGenerating(ByVal project As Project) _ Implements IWizard.ProjectFinishedGenerating Dim sharePointProject As ISharePointProject = presentationModel.ProjectService.Convert(Of Project, ISharePointProject)(project) sharePointProject.SiteUrl = New Uri(presentationModel.CurrentSiteUrl, UriKind.Absolute) sharePointProject.IsSandboxedSolution = presentationModel.IsSandboxed signingManager.AddKeyFile(project) End Sub ' Always return true; this IWizard implementation throws a WizardCancelledException ' that is handled by Visual Studio if the user cancels the wizard. Public Function ShouldAddProjectItem(ByVal filePath As String) As Boolean _ Implements IWizard.ShouldAddProjectItem Return True End Function ' The following IWizard methods are not used in this example. Public Sub BeforeOpeningFile(ByVal projectItem As ProjectItem) _ Implements IWizard.BeforeOpeningFile End Sub Public Sub ProjectItemFinishedGenerating(ByVal projectItem As ProjectItem) _ Implements IWizard.ProjectItemFinishedGenerating End Sub Public Sub RunFinished() Implements IWizard.RunFinished End Sub End Class
using EnvDTE; using Microsoft.VisualStudio.SharePoint; using Microsoft.VisualStudio.TemplateWizard; using System; using System.Collections.Generic; namespace ProjectTemplateWizard { public class SiteColumnProjectWizard : IWizard { private WizardWindow wizardUI; private DTE dteObject; private SiteColumnWizardModel presentationModel; private ProjectSigningManager signingManager; public SiteColumnProjectWizard() { signingManager = new ProjectSigningManager(); } public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams) { dteObject = automationObject as DTE; presentationModel = new SiteColumnWizardModel(dteObject, false); if (!presentationModel.ProjectService.IsSharePointInstalled) { string errorString = "A SharePoint server is not installed on this computer. A SharePoint server " + "must be installed to work with SharePoint projects."; System.Windows.MessageBox.Show(errorString, "SharePoint Not Installed", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); throw new WizardCancelledException(errorString); } wizardUI = new WizardWindow(presentationModel); Nullable<bool> dialogCompleted = wizardUI.ShowModal(); if (dialogCompleted == true) { replacementsDictionary.Add("$selectedfieldtype$", presentationModel.FieldType); replacementsDictionary.Add("$selectedgrouptype$", presentationModel.FieldGroup); replacementsDictionary.Add("$fieldname$", presentationModel.FieldName); signingManager.GenerateKeyFile(); } else { throw new WizardCancelledException(); } } // Populate the SiteUrl and IsSandboxedSolution properties in the new project, and add a new // key.snk file to the project. public void ProjectFinishedGenerating(Project project) { ISharePointProject sharePointProject = presentationModel.ProjectService.Convert<Project, ISharePointProject>(project); sharePointProject.SiteUrl = new Uri(presentationModel.CurrentSiteUrl, UriKind.Absolute); sharePointProject.IsSandboxedSolution = presentationModel.IsSandboxed; signingManager.AddKeyFile(project); } // Always return true; this IWizard implementation throws a WizardCancelledException // that is handled by Visual Studio if the user cancels the wizard. public bool ShouldAddProjectItem(string filePath) { return true; } // The following IWizard methods are not used in this example. public void BeforeOpeningFile(ProjectItem projectItem) { } public void ProjectItemFinishedGenerating(ProjectItem projectItem) { } public void RunFinished() { } } }
Creating the SharePoint Commands
Create two custom commands that call into the SharePoint server object model. One command determines whether the site URL that the user types in the wizard is valid. The other command gets all of the field types from the specified SharePoint site so that users can select which one to use as the basis for their new site column.
To define the SharePoint commands
In the SharePointCommands project, open the Commands code file.
Replace the entire contents of this file with the following code.
Imports Microsoft.SharePoint Imports Microsoft.VisualStudio.SharePoint.Commands Namespace Contoso.SharePoint.Commands Friend Class Commands <SharePointCommand(CommandIds.ValidateSite)> _ Private Function ValidateSite(ByVal context As ISharePointCommandContext, ByVal url As Uri) As Boolean Using site As SPSite = New SPSite(url.AbsoluteUri) Dim webUrl As String = DetermineWebUrl(url.AbsolutePath, site.ServerRelativeUrl) If webUrl IsNot Nothing Then Using web As SPWeb = site.OpenWeb(webUrl, True) Return web.Exists End Using End If End Using Return False End Function ' For simplicity, this command does not check to make sure the provided Uri is valid. ' Use the ValidateSite command to verify that the Uri is valid first. <SharePointCommand(CommandIds.GetFieldTypes)> _ Private Function GetFieldTypes(ByVal context As ISharePointCommandContext, ByVal url As Uri) As String() Dim columnDefinitions As List(Of String) = New List(Of String)() Using site As SPSite = New SPSite(url.AbsoluteUri) Dim webUrl As String = DetermineWebUrl(url.AbsolutePath, site.ServerRelativeUrl) Using web As SPWeb = site.OpenWeb(webUrl, True) For Each columnDefinition As SPFieldTypeDefinition In web.FieldTypeDefinitionCollection columnDefinitions.Add(columnDefinition.TypeName) Next ' SharePoint commands cannot serialize List<string>, so return an array. Return columnDefinitions.ToArray() End Using End Using End Function Private Function DetermineWebUrl(ByVal serverRelativeInputUrl As String, ByVal serverRelativeSiteUrl As String) As String ' Make sure both URLs have a trailing slash. serverRelativeInputUrl = EnsureTrailingSlash(serverRelativeInputUrl) serverRelativeSiteUrl = EnsureTrailingSlash(serverRelativeSiteUrl) Dim webUrl As String = Nothing Dim isSubString As Boolean = serverRelativeInputUrl.StartsWith(serverRelativeSiteUrl, StringComparison.OrdinalIgnoreCase) If isSubString Then ' The Web URL cannot have escaped characters. webUrl = Uri.UnescapeDataString(serverRelativeInputUrl.Substring(serverRelativeSiteUrl.Length)) End If Return webUrl End Function Private Function EnsureTrailingSlash(ByVal url As String) If Not String.IsNullOrEmpty(url) AndAlso url(url.Length - 1) <> "/" Then url += "/" End If Return url End Function End Class End Namespace
using System; using System.Collections.Generic; using Microsoft.SharePoint; using Microsoft.VisualStudio.SharePoint.Commands; namespace Contoso.SharePoint.Commands { internal class Commands { [SharePointCommand(CommandIds.ValidateSite)] private bool ValidateSite(ISharePointCommandContext context, Uri url) { using (SPSite site = new SPSite(url.AbsoluteUri)) { string webUrl = DetermineWebUrl(url.AbsolutePath, site.ServerRelativeUrl); if (webUrl != null) { using (SPWeb web = site.OpenWeb(webUrl, true)) { return web.Exists; } } } return false; } // For simplicity, this command does not check to make sure the provided Uri is valid. // Use the ValidateSite command to verify that the Uri is valid first. [SharePointCommand(CommandIds.GetFieldTypes)] private string[] GetFieldTypes(ISharePointCommandContext context, Uri url) { List<string> columnDefinitions = new List<string>(); using (SPSite site = new SPSite(url.AbsoluteUri)) { string webUrl = DetermineWebUrl(url.AbsolutePath, site.ServerRelativeUrl); using (SPWeb web = site.OpenWeb(webUrl, true)) { foreach (SPFieldTypeDefinition columnDefinition in web.FieldTypeDefinitionCollection) { columnDefinitions.Add(columnDefinition.TypeName); } // SharePoint commands cannot serialize List<string>, so return an array. return columnDefinitions.ToArray(); } } } private string DetermineWebUrl(string serverRelativeInputUrl, string serverRelativeSiteUrl) { // Make sure both URLs have a trailing slash. serverRelativeInputUrl = EnsureTrailingSlash(serverRelativeInputUrl); serverRelativeSiteUrl = EnsureTrailingSlash(serverRelativeSiteUrl); string webUrl = null; bool isSubString = serverRelativeInputUrl.StartsWith(serverRelativeSiteUrl, StringComparison.OrdinalIgnoreCase); if (isSubString) { // The Web URL cannot have escaped characters. webUrl = Uri.UnescapeDataString(serverRelativeInputUrl.Substring(serverRelativeSiteUrl.Length)); } return webUrl; } private string EnsureTrailingSlash(string url) { if (!String.IsNullOrEmpty(url) && url[url.Length - 1] != '/') { url += '/'; } return url; } } }
Checkpoint
At this point in the walkthrough, all the code for the wizard is now in the project. Build the project to make sure that it compiles without errors.
To build your project
- On the Build menu, select Build Solution.
Removing the key.snk File from the Project Template
In Walkthrough: Creating a Site Column Project Item with a Project Template, Part 1, the project template that you created contains a key.snk file that is used to sign each Site Column project instance. This key.snk file is no longer necessary because the wizard now generates a new key.snk file for each project. Remove the key.snk file from the project template and remove references to this file.
To remove the key.snk file from the project template
In Solution Explorer, under the SiteColumnProjectTemplate node, right-click the key.snk file and click Delete. In the message box that asks you to confirm, click OK.
Under the SiteColumnProjectTemplate node, open the SiteColumnProjectTemplate.vstemplate file.
Remove the following element from the file.
<ProjectItem ReplaceParameters="false" TargetFileName="key.snk">key.snk</ProjectItem>
Save and close the file.
Under the SiteColumnProjectTemplate node, open the ProjectTemplate.csproj or ProjectTemplate.vbproj file.
Remove the following PropertyGroup element.
<PropertyGroup> <SignAssembly>true</SignAssembly> <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile> </PropertyGroup>
Remove the following None element.
<None Include="key.snk" />
Save and close the file.
Associating the Wizard with the Project Template
Now that you have implemented the wizard, you must associate the wizard with the Site Column project template. There are three procedures you must complete to do this:
Sign the wizard assembly with a strong name.
Get the public key token for the wizard assembly.
Add a reference to the wizard assembly in the .vstemplate file for the Site Column project template.
To sign the wizard assembly with a strong name
In Solution Explorer, right-click the ProjectTemplateWizard project node and click Properties.
Click the Signing tab.
Select the Sign the assembly check box.
In the Choose a strong name key file drop-down list, select <New...>.
In the Create Strong Name Key dialog box, enter a name for the new key file and clear the Protect my key file with a password check box.
Click OK.
On the Build menu, select Build Solution.
To get the public key token for the wizard assembly
Open a Visual Studio Command Prompt window.
Run the following command. Replace path to wizard assembly with the full path to the built ProjectTemplateWizard.dll assembly for the ProjectTemplateWizard project on your development computer.
sn.exe -T path to wizard assembly
The public key token for the ProjectTemplateWizard.dll assembly is written to the Visual Studio Command Prompt window.
Keep the Visual Studio Command Prompt window open. You will need the public key token during the next procedure.
To add a reference to the wizard assembly in the .vstemplate file
In Solution Explorer, expand the SiteColumnProjectTemplate project node and open the SiteColumnProjectTemplate.vstemplate file.
Near the end of the file, add the following WizardExtension element between the </TemplateContent> and </VSTemplate> tags. Replace the your token value of the PublicKeyToken attribute with the public key token that you obtained in the previous procedure.
<WizardExtension> <Assembly>ProjectTemplateWizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=your token</Assembly> <FullClassName>ProjectTemplateWizard.SiteColumnProjectWizard</FullClassName> </WizardExtension>
For more information about the WizardExtension element, see WizardExtension Element (Visual Studio Templates).
Save and close the file.
Adding Replaceable Parameters to the Elements.xml File in the Project Template
Add several replaceable parameters to the Elements.xml file in the SiteColumnProjectTemplate project. These parameters are initialized in the RunStarted method in the SiteColumnProjectWizard class that you defined earlier. When a user creates a Site Column project, Visual Studio automatically replaces these parameters in the Elements.xml file in the new project with the values that they specified in the wizard.
A replaceable parameter is a token that begins and ends with the dollar sign ($) character. In addition to defining your own replaceable parameters, you can use built-in parameters that are defined and initialized by the SharePoint project system. For more information, see Replaceable Parameters.
To add replaceable parameters to the Elements.xml file
Open the Elements.xml file in the SiteColumnProjectTemplate project.
Replace the contents of this file with the following XML.
<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <Field ID="{$guid5$}" Name="$fieldname$" DisplayName="$fieldname$" Type="$selectedfieldtype$" Group="$selectedgrouptype$"> </Field> </Elements>
The new XML changes the values of the Name, DisplayName, Type, and Group attributes to custom replaceable parameters.
Save and close the file.
Adding the Wizard to the VSIX Package
To deploy the wizard with the VSIX package that contains the Site Column project template, add references to the wizard project and the SharePoint commands project to the source.extension.vsixmanifest file in the VSIX project.
To add the wizard to the VSIX package
In Solution Explorer, double-click the source.extension.vsixmanifest file in the SiteColumnProjectItem project.
Visual Studio opens the file in the manifest editor.
In the Content section of the editor, click the Add Content button.
In the Add Content dialog box, in the Select a content type list box, select Template Wizard.
Under Select a Source, click the Project radio button, and select ProjectTemplateWizard in the list box next to it.
Click OK.
In the manifest editor, click the Add Content button again.
In the Add Content dialog box, in the Select a content type list box, select Custom Extension Type.
Note
This value corresponds to the CustomExtension element in the extension.vsixmanifest file. This element specifies a custom extension that you want to include in the Visual Studio extension. For more information, see CustomExtension Element (VSX Schema).
In the Type text box, type SharePoint.Commands.v4.
Note
This value corresponds to the Type attribute of the CustomExtension element in the extension.vsixmanifest file. The value Sharepoint.Commands.v4 is required for all custom extension assemblies that contain custom SharePoint commands.
Under Select a Source, click the Project radio button, and select SharePointCommands in the list box next to it.
Click OK.
On the Build menu, click Build Solution. Make sure that the solution compiles without errors.
Testing the Wizard
You are now ready to test the wizard. First, start debugging the SiteColumnProjectItem solution in the experimental instance of Visual Studio. Then, test the wizard for the Site Column project in the experimental instance of Visual Studio. Finally, build and run the project to verify that the site column works as expected.
To start debugging the solution
Restart Visual Studio with administrator privileges and open the SiteColumnProjectItem solution.
In the ProjectTemplateWizard project, open the SiteColumnProjectWizard code file and add a breakpoint to the first line of code in the RunStarted method.
On the Debug menu, click Exceptions.
In the Exceptions dialog, make sure that the Thrown and User-unhandled check boxes for Common Language Runtime Exceptions are cleared.
Click OK.
Press F5 to start debugging.
Visual Studio installs the extension to %UserProfile%\AppData\Local\Microsoft\VisualStudio\10.0Exp\Extensions\Contoso\Site Column\1.0 and starts an experimental instance of Visual Studio. You will test the project item in this instance of Visual Studio.
To test the wizard in Visual Studio
In the experimental instance of Visual Studio, on the File menu, point to New, and then click Project.
Expand Visual C# or Visual Basic (depending on the language your project template supports), expand SharePoint, and then click 2010.
In the list of project templates, click Site Column.
In the Name box, type SiteColumnWizardTest.
Click OK.
Verify that the code in the other instance of Visual Studio stops on the breakpoint that you set earlier in the RunStarted method. Press F5 to continue to debug the project.
In the SharePoint Customization Wizard, type the URL of the site that you want to use for debugging, and click Next.
In the second page of the SharePoint Customization Wizard, make the following selections:
For Type, select Boolean in the drop-down list.
For Group, type Custom Yes/No Columns in the combo box.
For Name, type My Yes/No Column in the text box.
Click Finish.
A new project appears in Solution Explorer with a project item named Field1, and Visual Studio opens the Elements.xml file in the editor. Verify that Elements.xml contains the values that you specified in the wizard.
To test the site column in SharePoint
In the experimental instance of Visual Studio, press F5. The site column is packaged and deployed to the SharePoint site specified by the Site URL property of the project. The Web browser opens to the default page of this site.
Note
If the Script Debugging Disabled dialog box appears, click Yes to continue to debug the project.
On the Site Actions menu, click Site Settings.
Under Galleries, click Site columns.
In the list of site columns, verify that there is a Custom Yes/No Columns group that contains a column named My Yes/No Column.
Close the Web browser.
Cleaning up the Development Computer
After you finish testing the project item, remove the project template from the experimental instance of Visual Studio.
To clean up the development computer
In the experimental instance of Visual Studio, on the Tools menu, click Extension Manager.
The Extension Manager dialog box opens.
In the list of extensions, click Site Column, and then click Uninstall.
In the dialog box that appears, click Yes to confirm that you want to uninstall the extension.
Click Restart Now to complete the uninstallation.
Close both instances of Visual Studio (the experimental instance and the instance of Visual Studio that has the SiteColumnProjectItem solution open).
See Also
Tasks
Walkthrough: Creating a Site Column Project Item with a Project Template, Part 1
How to: Use Wizards with Project Templates
Reference
Visual Studio Template Schema Reference
Concepts
Defining Custom SharePoint Project Item Types
Creating Item Templates and Project Templates for SharePoint Project Items