Introduction
SharePoint 2010 is a very nice evolution from SharePoint 2007, and integrated with Visual Studio 2010 developer, productivity in SharePoint related projects has been greatly enhanced. Visual Studio 2010 contains a number of built-in templates and features that allow the developer to do with a few clicks of the mouse what had taken painful hand-coding to accomplish before. However, with all of the advances, there are still some areas that could use improvements.
One such area that could use refinement is the ability to import an existing list into a Site Definition project.
Given a scenario of creating a Site Definition, there may be a time when you have an existing list, possibly with data already in it, that can be imported into the project. With the methods currently available, this can become a long and tedious process. In this article, I'll explore the current methods and present an alternative using a Visual Studio SharePoint Project Extension.
Create Site Template
Visual Studio 2010 now includes a SharePoint 2010 project, Import SharePoint Solution Package, that allows you to import an existing site and all its items such as content types, site columns and lists into a Site Definition project.
The first step for this process is to create a template from an existing site via Site Actions -> Save
site as template. A drawback here is that it cannot be used for publishing sites.
After clicking this link, a form is presented requesting a name for the template and whether to include content, such as existing data in a list. Once saved, the template is placed in the Solutions Gallery.
The template file can be downloaded from the Solution Gallery by clicking on the template name, in this example: MySiteDef
.
Import SharePoint Solution Package
From within Visual Studio 2010, you can create a new project in the standard way and select Import SharePoint Solution Package from the New Project dialog.
After selecting the project, a series of wizard dialogs is displayed allowing you to choose the site and if the project is for a farm or sandboxed solution. The next dialog allows you to choose the location of the template file that was saved in the early step.
After clicking the Next button, Visual Studio will read the solution package specified and extracts information about the site and its contents.
As you can see, this list includes everything in the site. Although you can deselect the items you don't want to import, if it is a large site it can be a tedious process to find the items necessary. As a note, if you deselect a content type or field that a list relies on you will be warned to include it or ignore the warning. After the project has been created, you will need to have the site definition project you want to import the list into opened in the same solution. From there, you can copy the list definition folder from the imported project to the site definition project. This does not automatically copy any related objects such as fields or content types; they must be copied individually in the same manner.
As you can see, this is quite cumbersome for importing a single list definition.
Doing It By Hand
Another possibility is to do it all by hand. You can add a new List Definition item to the project and add the necessary elements to the schema.xml and elements.xml for the list. Of course, you must also do the same for any fields and content types necessary. If you are accustomed to SharePoint 2007 development this method should be familiar, and time consuming. For a small list with standard fields and no existing instance, this can be a viable option; however, the tools are available to do better.
Visual Studio Extension Package
In previous versions of Visual Studio, the way to add additional functionality was to create an Addin. Although this method is still supported, with Visual Studio 2010 the preferred method is to create a Visual Studio Extension package (VSIX) since it is built using the Managed Extensibility Framework. The first thing necessary to create an extension for SharePoint is to download and install the Visual Studio 2010 SDK. Once the SDK is installed, you will find an additional item in the New Project dialog under Other Project Types -> Extensibility.
Creating a Visual Studio Extension package can be a book in itself, so I will concentrate on discussing aspects related to our purpose, creating a Visual Studio 2010 SharePoint project extension.
Extension Manifest
After selecting this project type, you will be presented with a series of dialogs asking for information about the project, like a name and description, icon to use, what language and whether it supports menu commands. After these questions have been answered, the project will be created with several files already populated, most of them are not relevant to this discussion and are not strictly necessary to create an extension. They do have a remarkable amount of inline documentation explaining their purpose though. The one file we will concentrate on however is source.extension.vsixmanifest
. This XML file contains all the information necessary to install and configure the extension in Visual Studio as well as display information about it. A custom viewer is used to display it in a nice UI.
One section of interest is the Supported VS Editions. Clicking the Select Editions button will display this dialog to allow you to specify which edition of Visual Studio the extension will be supported. For the purpose of this article, the defaults are fine.
The other section of interest to us is the Content section. This allows you to add or remove assemblies from the package.
Clicking the Add Content button will display a dialog allowing you to select the type of content and where it is located. For this article, we are adding a MEF Component from another project in the solution.
Now, let's get to the implementation.
Import List Extension Implementation
The implementation assembly is a simple class library project. However, to have any classes in this assembly recognized by the MEF and Visual Studio 2010, you need to apply the System.ComponentModel.Composition.ExportAttribute
. Before you can do this, however, you need to add two references to the project, Microsoft.VisualStudio.SharePoint.dll and System.ComponentModel.Composition.dll. The latter contains the ExportAttribute
and the former contains the interfaces you will need to implement.
[Export(typeof(ISharePointProjectExtension))]
public class ImportListExtention : ISharePointProjectExtension
{
public void Initialize(ISharePointProjectService projectService)
{
}
}
As you can see from this snippet, there is only one method that needs to be implemented from the ISharePointProjectExtention
interface: Initialize
. In this method, you can subscribe to a number of events on the ISharePointProjectService
object that is passed to it. For this purpose, I'll subscribe to the ProjectMenuItemsRequested
event. This event occurs when the context menu is about to be displayed when right-clicking a SharePoint project in the Solution Explorer window. In the handler for this event, you can either add a menu item to the Actions area of the context menu, using the ActionMenuItems
collection, or in this case, to the AddMenuItems
collection, which will add the item to the Add submenu.
void OnProjectMenuItemsRequested
(object sender, SharePointProjectMenuItemsRequestedEventArgs e)
{
IMenuItem menu = e.AddMenuItems.Add("Import List");
menu.Click += new EventHandler<menuitemeventargs />(OnMenuClick);
}
void OnMenuClick(object sender, MenuItemEventArgs e)
{
ImportList import = new ImportList(e.Owner as ISharePointProject);
import.Import();
}
Import List
After you click the Import List menu item, a dialog is displayed to gather more information about the list to import. In order to get the lists available from the specified site, you need use the SharePoint server object model via SharePoint Commands.
SharePoint Commands
SharePoint cannot be accessed directly from within Visual Studio, so you must create commands to execute through the ISharePointConnection
implementation available from the SharePointProject
object connected to the SharePoint project in Visual Studio. The purpose of the ISharePointConnection
interface is to allow access to SharePoint via the 64-bit Visual Studio process, vssphost.exe.
The commands are built in a separate assembly which must be compiled against .NET Framework 3.5. This point eluded me initially and caused a few hours of head scratching. The assembly will compile and run using the .NET Framework 4.0; however, an exception will be generated when calling ExecuteCommand,
seemingly indicating that the command string has not been defined.
Commands are defined by decorating a method with the SharePointCommandAttribute
and supplying a name for the command. When using the ExecuteCommand
method of the ISharePointConnection
object, this same string
must be supplied as the first argument.
Project.SharePointConnection.ExecuteCommand<string>
(SharePointCommands.Commands.GetLists, SiteURL);
Some restrictions on ISharePointConnection.ExecuteCommand
are that, other than the command name, only one other parameter can be passed to it and both input and return objects must be serializable. ExecuteCommand
is a templated method which allows you to specify the type of the input and return value, string
and List<string>
respectively in the above case. We are passing a string
, the URL of the site to retrieve lists from and returning with a collection of string
s representing the list names.
[SharePointCommand("MANSoftDev.Commands.GetLists")]
public static List<string> GetLists(ISharePointCommandContext context, string siteUrl)
{
List<string> lists = new List<string>();
using(SPSite site = new SPSite(siteUrl))
{
using(SPWeb web = site.OpenWeb())
{
foreach(SPList item in web.Lists)
{
if(item.Hidden == false)
{
lists.Add(item.Title);
}
}
}
}
return lists;
}
The implementation of the SharePointCommand
method receives an ISharePointCommandContext
object from the ISharePointConnection
instance that is making the call. This object provides access to the SharePoint context the command will be executed against and contains properties for the Site and Web as well as a Logger allowing messages to be written to the output window in Visual Studio.
The implementation of the GetLists
command is very straight forward. We create a SPSite
from the URL that is passed in and iterate through the visible SPList
s in the Web, then return a collection of the names of each list. Creating the files for the list definition and instance becomes more complicated though.
Create List Definition and Instance
To get information necessary to construct the files, you must supply the site URL and list name. However, since only one parameter can be passed to the ExecuteCommand
method, you need to use a wrapper object.
[Serializable]
public struct GetListData
{
public string SiteUrl { get; set; }
public string ListName { get; set; }
public bool IncludeContent { get; set; }
}
project.SharePointConnection.ExecuteCommand<sharepointcommands.getlistdata>
(SharePointCommands.Commands.GetListData, data);
Although it would be nice to return a SPList
from a SharePointCommand
, it is not serializable. This means everything necessary to create the definition and instance files must be created in the command implementation method. The good thing is most of the information is readily available from the SPList
object.
Schema, List Definition and List Instance Files
The list schema file describes the list's content type, fields, view and more. The basic structure of this file is as follows:
<List>
<MetaData>
<ContentTypes />
<Fields />
<Views>
<View>
<Query />
</View>
</Views>
</MetaData>
</List>
The View
and Fields
elements are easy to obtain from the SPList
.
string views = list.Views.SchemaXml;
string fields = list.Fields.SchemaXml;
The ContentType
needs just a bit more to extract the required XML without any extra bits:
string contentTypes = "<contenttypes>";
foreach(SPContentType item in list.ContentTypes)
{
contentTypes += item.Parent.SchemaXmlWithResourceTokens;
}
contentTypes += "</contenttypes>";
From here, it's just a matter of constructing the XML file:
XElement schema = new XElement(ns + "List",
new XAttribute("Title", title),
new XAttribute("Direction", "none"),
new XAttribute("Url", "Lists/" + url),
new XAttribute("BaseType", baseType),
new XAttribute("Type", templateType),
new XAttribute("BrowserFileHandling", "Permissive"),
new XAttribute("FolderCreation", "FALSE"),
new XAttribute("Catalog", "FALSE"),
new XAttribute("SendToLocation", "|"),
new XAttribute("ImageUrl", "/_layouts/images/itgen.png"),
new XAttribute(XNamespace.Xmlns + "ows", "Microsoft SharePoint"),
new XAttribute(XNamespace.Xmlns + "spctf",
"http://schemas.microsoft.com/sharepoint/v3/contenttype/forms"),
new XElement("MetaData",
contentTypesXML,
fieldsXML,
CreateFormsElement(),
CleanViews(viewsXML)
)
);
and cleaning up the View
element slightly:
private static XElement CleanViews(XElement views)
{
views.Elements("View").Attributes("Name").Remove();
foreach(XElement view in views.Elements("View"))
{
view.SetAttributeValue("Url", "AllItems.aspx");
view.SetAttributeValue("SetupPath", @"pages\viewpage.aspx");
view.SetAttributeValue("WebPartZoneID", "Main");
}
return views;
}
The list definition and instance are very easy to create:
private static XElement CreateListTemplate
(string name, int templateType, int baseType, string displayName, string description)
{
XNamespace ns = "http://schemas.microsoft.com/sharepoint/";
XElement xml = new XElement(ns + "Elements",
new XElement("ListTemplate",
new XAttribute("Name", name),
new XAttribute("Type", templateType),
new XAttribute("BaseType", baseType),
new XAttribute("OnQuickLaunch", "TRUE"),
new XAttribute("SecurityBits", "11"),
new XAttribute("Sequence", "410"),
new XAttribute("DisplayName", displayName),
new XAttribute("Description", description),
new XAttribute("Image", "/_layouts/images/itgen.png"))
);
return XElement.Parse(xml.ToString().Replace("xmlns=\"\"", ""));
}
private static XElement CreateListInstance
(string title, int templateType, string url, string description)
{
XNamespace ns = "http://schemas.microsoft.com/sharepoint/";
XElement xml = new XElement(ns + "Elements",
new XElement("ListInstance",
new XAttribute("Title", title + " Instance"),
new XAttribute("OnQuickLaunch", "TRUE"),
new XAttribute("TemplateType", templateType),
new XAttribute("Url", "Lists/" + url),
new XAttribute("Description", description))
);
return XElement.Parse(xml.ToString().Replace("xmlns=\"\"", ""));
}
Again, since the SPList
object is not serializable, you need to use a wrapper object to return this information back to the caller.
[Serializable]
public struct ListData
{
public string ListName { get; set; }
public XElement Schema { get; set; }
public XElement ListInstance { get; set; }
public XElement ListTemplate { get; set; }
public bool IsError { get; set; }
public string ErrorMessage { get; set; }
}
Adding SharePointCommand to Package
Now that the SharePoint command assembly has been constructed, it needs to be added to the Visual Studio Extension package. Just as you added the MEF component to the package, the SharePointCommand
needs to be added so it will be deployed. As you can see, the content type for this assembly is Custom Extension Type
and the type is set to SharePoint.Commands.v4
. This key for the assembly to be recognized as containing commands and be accessible.
Adding to Visual Studio
Now that all of the necessary files have been created, you need to add everything to the Visual Studio project. This is relatively straight forward using the ISharePointProject
reference that represents the current project in Visual Studio. Just like a non-SharePoint project, a collection of all files and folders is maintained by the project and can be referenced, in this case, using the ISharePointProjectItemsCollection
. Although you could just add the files directly to project tree, this won't work for a SharePoint project. SharePoint projects use a file named SharePointProjectItem.spdata
to maintain meta data about the files in a folder and how to process them. This file is not included in the Solution Explorer tree unless you select View All Files.
<ProjectItem Type="Microsoft.VisualStudio.SharePoint.ListDefinition"
DefaultFile="Elements.xml"
SupportedTrustLevels="All"
SupportedDeploymentScopes="Web, Site"
xmlns="http://schemas.microsoft.com/VisualStudio/2010/
SharePointTools/SharePointProjectItemModel">
<Files>
<ProjectItemFile Source="Elements.xml" Target="CustomList\" Type="ElementManifest" />
<ProjectItemFile Source="Schema.xml" Target="CustomList\" Type="ElementFile" />
</Files>
</ProjectItem>
The key elements in this file are the type attributes. For the ProjectItem
element, it is set to Microsoft.VisualStudio.SharePoint.ListDefinition
telling Visual Studio what type of items are included in the folder. For the ProjectItemFiles
elements, it is set to ElementManifest
and ElementFile
respectively, indicating to Visual Studio how to process the files when deploying the solution.
Using the ISharePointProjectItemsCollection
we first add the folder, specifying the type as Microsoft.VisualStudio.SharePoint.ListDefinition
, to the collection which returns a ISharePointProjectItem
object. Since the project items are physical files within the project tree, you must write the XML that was returned from the SharePointCommand
call to file before it can be added to the project.
ISharePointProjectItem item = project.ProjectItems.Add(listData.ListName, LIST, false);
string folder = Path.GetDirectoryName(item.FullPath);
string fileName = Path.Combine(folder, "Schema.xml");
WriteFile(fileName, listData.Schema);
ISharePointProjectItemFile file = item.Files.AddFromFile(fileName);
file.DeploymentType = DeploymentType.ElementFile;
private void WriteFile(string fileName, XElement element)
{
XmlWriterSettings xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = false;
xws.Indent = true;
using(XmlWriter writer = XmlWriter.Create(fileName, xws))
{
element.WriteTo(writer);
}
}
Completion
All of the files have now been added to the SharePoint project with the appropriate metadata set so that Visual Studio will know how to process each during deployment.
Obviously there are settings that may need to be adjusted manually; however, you must admit it is much easier to make these adjustments rather than to create all the files manually.
Room for Improvement
This solution could be made better by adding any custom site columns and/or content types to the project when the list is being imported. This is a more complicated procedure that requires checking each site column against the builtin sites columns to determine if it needs to be added, then checking to ensure it has not already been included in the project. The same applies for content types. With the information that has been provided, you should be able to understand the steps necessary to add this functionality.
Resources
History