Saturday, 28 April 2007

Deploying master pages and page layouts as a feature

This is the fourth article in my series of how to create common SharePoint site artifacts as features. Last time we looked at deploying content types as a feature.

Now that we have content types and all the supporting artifacts deployed, we can focus on deploying the things that make a real difference to how our site works - master pages and page layouts. I'll also cover web part deployment in a forthcoming post.

The basic premise is:-

  • develop master pages/layouts using SharePoint Designer (SPD)
  • save each file to the filesystem (as opposed to the Master Page Gallery) ready to be added to a feature. Unfortunately, SPD will actually screw up hyperlinks and some control references when you do this - in particular references to user controls using the '~/_controltemplates/' path. The best solution is to copy the contents of the final file into notepad, and save the file to the filesystem with the same name.
  • create the feature.xml and element manifest file for the feature. An example of an elements manifest file to deploy 2 master pages and 3 page layouts is shown at the end of this post.

Let's talk through the values used in the example below. At the module level (collection of files to deploy to a particular location in SharePoint), we specify the URL and whether the files should only be deployed to the root web or to all webs in the site collection. At the file level, attributes for the master pages are fairly simple:-

  • 'IgnoreIfAlreadyExists' - should be true if we want to overwrite an existing file of this name, false if not.
  • 'Type' - should be 'GhostableInLibrary' for files which exist in a document library such as the Pages, Style Library or Master Page Gallery libraries which exist in a SharePoint publishing site.

For the page layouts, things are slightly more complex. Here, we need to specify the following:-
  • 'ContentType' - specifies whether the file is a page layout or master page. Use the value '$Resources:cmscore,contenttype_pagelayout_name;' to specify SharePoint's internal string which represents the page layout option.
  • 'PublishingPreviewImage' - path to URL accessible image file to be displayed when this layout is selected in the listbox when creating a page.
  • 'PublishingAssociatedContentType' - this is where we specify which content type the layout should be associated with. This means the layout will automatically have this binding and will be ready for use. Note that if this value is omitted, by default your layout will be associated with the basic 'Page' content type from the publishing feature. This means any custom columns you have added will not be available. The value for this property should be in form ';#<Content type name>;#<Content type ID;#>. So a real example would be ';#Welcome Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4;#'.


Another interesting facet of deploying files to SharePoint in this way, is that it's not possible (AFAIK) to update a file deployed from feature 'A' by a separate feature 'B'. This makes sense but gets in the way if you want to update a common file like itemStyle.xsl (used by the Content Query web part) using a feature. This won't work since the file was originally provisioned by the PublishingResources feature, not your custom feature.

I've also seen problems updating page layouts which were associated with content types created through the UI rather than by a feature.

So now that we have our lists, site columns, content types and master pages/page layouts deployed, we can create pages using the layouts and add content to the site.

Next time I'll talk about options around deploying web parts.

Here's the XML sample mentioned earlier:-

<?xml version="1.0" encoding="utf-8"?>
<elements xmlns="http://schemas.microsoft.com/sharepoint/">
<module name="MasterPagesModule" url="_catalogs/masterpage" rootwebonly="True" path="">
<file url="cScape.master" ignoreifalreadyexists="TRUE" type="GhostableInLibrary"></file>
<file url="accessible.master" ignoreifalreadyexists="TRUE" type="GhostableInLibrary"></file>
</module>
<module name="PageLayoutsModule" url="_catalogs/masterpage" rootwebonly="True" path="">
<file url="AdvancedSearchLayout.aspx" ignoreifalreadyexists="TRUE" type="GhostableInLibrary">
<property name="ContentType" value="$Resources:cmscore,contenttype_pagelayout_name;"></property>
<property name="PublishingPreviewImage" value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleLinks.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleLinks.png"></property>
<property name="PublishingAssociatedContentType" value=";#Welcome Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4;#"></property>
</file>
<file url="HelplineHomeLayout.aspx" ignoreifalreadyexists="TRUE" type="GhostableInLibrary">
<property name="ContentType" value="$Resources:cmscore,contenttype_pagelayout_name;"></property>
<property name="PublishingPreviewImage" value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleLinks.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleLinks.png"></property>
<property name="PublishingAssociatedContentType" value=";#RNIB Welcome;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4007AC2318FCE0E474eADE554A22E4B6135;#"></property>
</file>
<file url="HomePage.aspx" ignoreifalreadyexists="TRUE" type="GhostableInLibrary">
<property name="ContentType" value="$Resources:cmscore,contenttype_pagelayout_name;"></property>
<property name="PublishingPreviewImage" value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleLinks.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleLinks.png"></property>
<property name="PublishingAssociatedContentType" value=";#RNIB Welcome;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4007AC2318FCE0E474eADE554A22E4B6135;#"></property>
</file>
</elements>

Friday, 20 April 2007

Feature to create lookup fields on Codeplex

In a recent post I posted some sample code for a feature receiver which would create lookup fields (site columns which get their data from lists). A couple of people left comments asking for full set of files.

I've put these on Codeplex at http://www.codeplex.com/SP2007LookupFields.

Some notes:-

  • I've enhanced the solution to be more generic and deal with creating multiple lookup fields in one feature. Now the name of the list can be included in the CAML and the feature receiver will parse this, find the list and fix the reference via the list GUID using the API. Note currently the list must be in the root web, though it would be trivial to extend this.
  • All the hardcoded values have been removed, e.g. the path to the file containing the CAML definition is now passed as a feature property.
  • I mentioned in the earlier post that you could have a dependent feature so that the assembly containing the feature receiver also gets deployed automatically on feature activation. This doesn't quite make sense since assemblies can only be deployed using SharePoint solutions not features. Hence I've wrapped the feature in a SharePoint solution which deploys the assembly and feature. When the feature is activated the assembly is already in the GAC and the feature receiver runs happily. Note it would also be fairly simple to enhance the solution such that the assembly gets deployed to a site bin with appropriate CAS policy for highly-controlled environments.
  • I've also included an STSADM script to take care of deploying the solution - you need to edit the URL in this file to point to your SharePoint site.

Hope this is useful, let me know if you have feedback..

Tuesday, 17 April 2007

Deploying content types as a feature

This is the third article in my series of how to create common SharePoint site artifacts as features. Last time we looked at creating site columns as a feature.

Site columns are typically used in lists and/or content types. The concepts are fairly simple since a content type is also effectively a list with a series of columns (for those who have worked with CMS2002 the equivalent entity was a template definition). The columns give the data entered into content areas on a page somewhere to be stored. However, content types in SharePoint are also a valuable unit of granularity, since workflow, document lifecycle policy and several other key pieces of functionality can be configured at this level.

Any page layout you create as a template for pages on your site must be associated with a content type. Generally each column in the content type will be matched to a field control on the page layout, allowing the author to enter content to be stored in each column.

Fortunately creating content types as features is fairly simple - CAML only, no need for a feature receiver (code). Alas VSeWSS can't help us much here but don't forget to use the CAML schema intellisense in VS by ensuring the XML file we're about to write is linked to the schema at C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\XML\wss.xsd.

Initially, the part that can seem complex is the ID structure for a content type. Having been happily using GuidGen in Visual Studio every time we need an ID for something in a feature (though never trusting the first one ;-)), it comes as a surprise to have to read documentation just about IDs. As explained in Content Type IDs, the structure reflects the ancestry/parentage of a content type, meaning the parent content types can be determined very efficiently since simple string/byte matching can be used thus reducing database lookups.

The basics are:

  • find the ID of the content type you are deriving from. This can be done by either examining the content type in the SharePoint UI (Site Settings > Manage Content Types) and copying the ID from the URL querystring. Alternatively, search the feature files which the MS developers used to deploy the out-of-the-box content types. The former is probably simpler but the latter gives more scope for learning.
  • Add '00' and then a GUID you have generated (i.e. with GuidGen) to the end (suffix). You now have a valid content type ID. Note that you'll get a meaningful exception on feature activation if it's not.
  • For any child content types, to generate their IDs you can now add a simple ID such as '01' or '02' to the ID generated in the previous step. It's not necessary to suffix the ID with '00' and another GUID now since your unique ID is in the string. This means any ID's you generate will be different from anyone else's, so you can use the simple option and use a 2 digit number rather than another GUID. This means your content type IDs shouldn't grow too long.

The rest is fairly simple. Just add a FieldRef element for each site column the content type uses, specifying it's ID and name. Note that the approach we used to create our site columns meant that we got to specify the ID for them in CAML. We just need to dig out the IDs we specified there. So in both features, the IDs are in easily edited XML rather than being in any compiled code or similar so they are fairly loosely-coupled. Of course in many cases you'd choose to have both artifacts in the same feature, and in the future I'll post about factoring with relation to features.

You should end up with something like for each content type you are deploying:-


<!-- this content type is derived from the 'Page' content type from the 'Publishing' feature -->
<ContentType ID="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900CF38CCD7FC6E8247AA124F3EE5796C20"
Name="COBArticle"
Group="COB demo content types"
Description="Base content type for articles."
Version="0">
<FieldRefs>
<FieldRef ID="{ae8e6ab8-b151-4fa4-8694-3cd24ad3b1bc}" Name="Locations" />
<FieldRef ID="{A4983A93-4B14-4a61-AE08-09108A718628}" Name="Sectors" />
<FieldRef ID="{6F26090A-C2AE-44d7-8F70-EE1663FE29F1}" Name="Disciplines" />
<FieldRef ID="{71316CEA-40A0-49f3-8659-F0CEFDBDBD4F}" Name="Article Date" />
</FieldRefs>
</ContentType>

Note that it's not necessary to repeat the fields declared in parent content types, though I notice some of the Microsoft features do this.

So now you have your content types deployed, they can now be used in document libraries/lists or associated with any page layouts you have. So next time is deploying master pages/page layouts/CSS etc. as a feature, including having the layouts automatically bound to the content types.

Sunday, 15 April 2007

Sample code - creating lookup columns as a feature

I promised in an earlier post to post the code I used to create lookup columns as a feature. The basic idea here to use CAML as far as possible to define the site column, but use code to inject the list ID which the column refers to (unknown at feature design-time since SharePoint decides the GUID for lists). Extracted below is the code for the feature receiver.

A couple of key points:-

  • this assumes a file exists at the location D:\COB.Demo.ListBasedSiteColumns.Fields.xml with the following contents (note the 'List' attribute with an empty value):-
    <?xml version="1.0" encoding="utf-8"?>
    <elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <!-- _filecategory="ContentType" _filetype="File" _filename="fields.xml" _uniqueid="c0188da6-e320-44c0-b50c-cb6eaecec512" -->
    <Field
    Type="Lookup"
    Displayname="Locations"
    Required="FALSE"
    List=""
    ShowField="LinkTitleNoMenu"
    UnlimitedLengthInDocumentlibrary="FALSE"
    Group="COBDemo"
    ID="{ae8e6ab8-b151-4fa4-8694-3cd24ad3b1bc}"
    SourceId="{8c066b26-5a3e-4e1b-85ec-7e584cf178d7}"
    StaticName="Locations"
    Name="Locations">
    </elements>

  • Since most of the column definition is in CAML, this allows you to specify the ID of the column. This is useful later on when we deploy content types or lists which need to reference this site column by ID.
  • Site columns cannot be deleted when they are in use i.e. used in content types or lists. I prefer to try to delete the column on feature deactivation or reactivation, but omit error-handling so SharePoint throws an exception. This makes it clear to me why the feature cannot be deactivated. See comments in code for more information.
  • Note that the assembly which contains the code below must be available (in the GAC, or site bin directory with appropriate CAS policy) when the feature is activated. You must also specify the FeatureAssembly and FeatureReceiver attributes in your feature.xml file to register the feature receiver.
  • In terms of deploying the assembly, note that whilst you certainly can deploy assemblies to the GAC or site bin using the feature framework, you can't do it in this feature. Despite the 'FeatureActivated' name of the event handler, when your code executes the CAML won't yet have been processed. Hence your assembly won't yet have been copied and you'll get a FileNotFoundException when it tries to load the assembly. A good solution is to use a feature dependency. Here you can create a 2nd feature which deploys the assembly, and set the main feature to be dependent on this one. Additionally you can mark the assembly deployment feature to be hidden thus making the implementation a bit tidier. If it's useful to see the entire set of files I used leave me a comment and I'll put them up somewhere.
Apologies for the formatting, I will try to do something about this when I get time:-


public class FeatureReceiver : SPFeatureReceiver
{
private readonly string f_csSITE_COLS_DEFINITION_PATH = @"D:\COB.Demo.ListBasedSiteColumns.Fields.xml";

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
// feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
using (SPSite site = properties.Feature.Parent as SPSite)
{
SPWeb currentWeb = null;
Guid gRootWebId = Guid.Empty;
if (site != null)
{
currentWeb = site.RootWeb;
gRootWebId = currentWeb.ID;
}
else
{
currentWeb = properties.Feature.Parent as SPWeb;
gRootWebId = currentWeb.Site.RootWeb.ID;
}





using (currentWeb)
{
// get reference to the list..
SPList referencedList = currentWeb.Site.RootWeb.Lists["LocationsList"];
string sFieldElement = null;
XmlTextReader xReader = new XmlTextReader(f_csSITE_COLS_DEFINITION_PATH);
while (xReader.Read())
{
if (xReader.LocalName == "Field")
{
sFieldElement = xReader.ReadOuterXml();
break;
}
}


string sFinalCaml = replaceListGuidString(sFieldElement, referencedList);
createLookupColumn(currentWeb, sFinalCaml, "Locations");
currentWeb.Update();
}
}
}


private string replaceListGuidString(string sFieldElement, SPList referencedList)
{
string sPopulatedGuid = string.Format("List=\"{0}\"", referencedList.ID);
return sFieldElement.Replace("List=\"\"", sPopulatedGuid);
}

/// <summary>
/// Attempt to delete the column. Note that this will fail if the column is inuse,
/// i.e. it is used in a content type or list. I prefer to not catch the exception
/// (though it may be useful to add extra logging), hence feature deactivation/re- /// activation will fail. This effectively means this feature cannot be deactivated whilst the column is in use.
/// </summary>
/// <param name="column">Column to delete.</param>

private void attemptColumnDelete(SPFieldLookup column)
{
try
{
column.Delete();
}
catch (SPException e)
{
// consider logging full explanation..
throw;
}
}






private void createLookupColumn(SPWeb web, string sColumnDefinitionXml, string sColumnName)
{
// delete the column if it exists already and is not yet in use..
SPFieldLookup lookupColumn = null;
lookupColumn = web.Fields[sColumnName] as SPFieldLookup;
if (lookupColumn != null)
{
attemptColumnDelete(lookupColumn);
}

// now create the column from the CAML definition..
string sCreatedColName = web.Fields.AddFieldAsXml(sColumnDefinitionXml);

// also set LookupWebId so column can be used in webs other than web which hosts list..
lookupColumn = web.Fields[sCreatedColName] as SPFieldLookup;
lookupColumn.LookupWebId = web.Site.RootWeb.ID;
lookupColumn.Update();
}


public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
// delete the column if it exists already and is not yet in use..



// feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
using (SPSite site = properties.Feature.Parent as SPSite)
{
SPWeb currentWeb = null;
Guid gRootWebId = Guid.Empty;
if (site != null)
{
currentWeb = site.RootWeb;
gRootWebId = currentWeb.ID;
}
else
{
currentWeb = properties.Feature.Parent as SPWeb;
gRootWebId = currentWeb.Site.RootWeb.ID;
}

SPFieldLookup lookupColumn = null;
lookupColumn = currentWeb.Fields["LocationsList"] as SPFieldLookup;

if (lookupColumn != null)
{
attemptColumnDelete(lookupColumn);
}
}
}



public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
}


public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
}


If it's useful to have the full set of files illustrating the 2 features(particularly since the above isn't very readable!), leave me a comment and I'll make them available somewhere..

[Update - these have now been uploaded to Codeplex, see http://sharepointnutsandbolts.blogspot.com/2007/04/feature-to-create-lookup-fields-on.html]

Tuesday, 20 March 2007

How to debug SharePoint feature receivers

Just a quick one to detail how this is done in case someone finds it useful. For those still getting into working with SharePoint features, a feature receiver is a class which contains some code you've written to execute when a feature gets activated. Or deactivated, installed or uninstalled.

The key thing to note is that it's just standard ASP.Net debugging. The process is:-

  • deploy the assembly to the runtime location, either the GAC or the site bin directory. Note that if it's the bin directory your feature will also need appropriate CAS policy to grant the code the permissions it requires.
  • deploy the .pdb file to the same location. If this is the GAC, you can do the following:-

    - map a drive to the GAC folder i.e. C:\WINDOWS\assembly but using a UNC path such as [MachineName]\C$\WINDOWS\assembly. This allows you to browse the GAC without the shell which the framework puts on the folder, thus allowing you to see the actual structure of the files on disk.
    - locate the GAC_MSIL subfolder. In here you will see a directory for each assembly currently stored in the GAC. Find the directory for your assembly, and add the .pdb file so it sits next to the dll.


  • In Visual Studio, attach the debugger to the w3wp.exe process. Note that occasionally there will be 2 of these processes (e.g. when the process is being recycled), and it's possible to attach to the wrong one. Either do an IISReset to stop them both so that only 1 spins up with the next web request, or type 'iisapp' at the command prompt to get the process IDs of the running w3wp.exe processes. You can then match the correct one to the list which appears in the 'Attach debugger' dialog.
  • Activate the feature through the web UI (Site settings > Site Collection features/Site features). The debugger will now stop on any breakpoints you set.

And remember that the assembly must be built in debug mode so that the symbols are created.

Friday, 9 March 2007

Creating lookup columns as a feature

This is the second article in a series aimed at explaining the process of creating a MOSS site using SharePoint features. For the full series contents, see my introduction.

Last time we looked at the process of creating some SharePoint lists using VSeWSS. Sure, creating a list is a simple end-user task using the SharePoint UI, but in some scenarios such as when your site is a highly controlled internet/WCM site (or generally anywhere where we have multiple environments for dev/QA/staging/production) this deployment technique doesn't really cut it. Instead we probably want to use something more automated and repeatable than recreating such site artifacts manually each time. SharePoint 2007 supports this with Features.

Today we'll look at creating site columns which get their data from lists (lookup columns). This is a fairly common set-up, used for things like assigning metadata to a page or performing some other classification using a restricted set of values. Often the user would select the appropriate value using a dropdown shown when creating/editing a page.

Note that a similar method is to define a site column with several CHOICE elements, as below:-



However, this doesn't offer the same functionality as retrieving the values from a list. Consider the following:-

  • lists can have multiple columns whereas a choice element is effectively just one column
  • lists can have item-level permissions
  • lists can have events, workflow, versioning etc etc.

So it's clear many scenarios are better served from a site column which gets it's data from a list.

Now, when creating site artifacts as a feature, the developer will typically construct the definition in CAML, or allow VSeWSS to do this for him/her. Sometimes however, it's just not possible to do what you want with CAML. In these cases, the solution is to use a feature receiver. This is a class in compiled code (hopefully you still remember this ;-)) in which you override some methods and use the SharePoint API to define what should happen when the feature gets activated.

So why is it not possible to create a lookup column with CAML? After all, the following fragment successfully creates a site column which gets it's data from the list with the GUID specified in the 'List' attribute:-

<field id="{ae8e6ab8-b151-4fa4-8694-3cd24ad3b1bc}" type="Lookup" displayname="Locations" required="FALSE" list="{853CEC87-259E-47CA-97A7-42630F882FB7}" showfield="LinkTitleNoMenu" unlimitedlengthindocumentlibrary="FALSE" group="COB Metadata" sourceid="{8c066b26-5a3e-4e1b-85ec-7e584cf178d7}" staticname="Locations" name="Locations">

The answer is because list GUIDs are not deterministic. When a list gets created, whether through the UI, CAML or the API, it's GUID is assigned by SharePoint. There is no way to create a list with a GUID you have assigned. And if a list gets a new GUID each time it's created, this means it will have a different GUID in each of your dev/QA/staging/production environments. If this is your scenario suddenly that CAML fragment isn't so useful. Using this technique I would have to update the list GUID in my site column's <field> element and rebuild my site column feature every time the list got deployed to a new environment (or even redeployed). Clearly, this isn't pretty since, in addition to the extra effort, you're no longer deploying the exact same thing to live that has been tested in staging.

Hence I'd suggest any artifacts which reference a list should not refer to it in a declarative CAML. A better idea is to dynamically retrieve the list's GUID using the API (i.e. in a feature receiver), and create your site column in code. Briefly, the technique I use is this:

  • define site column in CAML using the fragment above
  • in my feature receiver, read this XML into memory
  • replace the list GUID with the real value retrieved from the API
  • create the site column using SPWeb.Fields.AddFieldAsXml(sXml) where sXml is the XML <field> element as a string
So this is kind of a cross between creating the site column in CAML and creating it in code. This gives a certain amount of flexibility since other properties of the column can be changed without having to recompile the code (simply change the value in the CAML, next time the feature runs it will read the modified values).

Note one other thing you are likely to need to do is to set the LookupWebId property on the column. This allows your column to be used in different sites in your site collection, yet still correctly reference the same list in your (for example) root site.

I'll post some sample code to do this in forthcoming post.

Assuming your CAML and the code to find the ID of your list was valid, you should see that you now have a site column which gets it's data from the list you specified when the feature is activated:-


One thing to note is that it's not really possible to delete the site column when the feature is deactivated. Generally, tidying up in this way is something you should do, but a site column cannot be deleted when it is has been provisioned on a list and has data. In this case, it's valid to not do any work on feature deactivation.

We're now well on our way to having page layouts which use content types with lookup columns. Phew! Next time, creating content types as a feature.

Friday, 2 March 2007

Creating lists with VSeWSS

This is the first article in a series aimed at explaining the process of creating a MOSS site using SharePoint features. For the full series contents, see my introduction.

Here, we'll look at creating SharePoint lists. For something so simple and core to SharePoint, they're surprisingly difficult to create as a feature. Fortunately Visual Studio extensions for WSS simplifies the process dramatically. If you've not come across this yet I'd recommend trying it - WseWSS is basically a huge help for some SharePoint development scenarios. The following articles are good background:-

Briefly, the process of creating a list in VSeWSS is as follows:

  1. Create a blank Visual Studio project. N.B. if you're actually creating a SharePoint site definition, you should select 'Team Site Definition' or 'Blank Site Definition', as WSeWSS will set you up with provisioning code which will execute whenever a site gets created from this definition.

  2. Select 'Add New Item' on the Project menu, then select 'List Definition' from the SharePoint section.

  3. Typically you should select 'Custom list' as the base list definition from the dropdown. Only select another choice if you're actually extending that list type and want to retain the original's columns. Assuming you want to create the actual list in addition to a list template (more on this later), check the 'Create an instance of this list' box. Note also the option to 'Add with event receiver' - this sets up code which which can be used to handle events on the list, for example when list items are added/edited/deleted.



  4. After clicking OK in the dialog, name your list appropriately. My recommendation would be to avoid spaces or other characters which might get encoded somewhere - the list will have a display name which you can be freer with.

    WSeWSS has now created several files in your VS project.




    We can see the following:-


    • some aspx files, the pages users will use to work with your list. Note these can be customised if you want to deviate from the standard behaviour.

    • instance.xml - this is the file to edit to add initial data to your list. Since this file is automatically linked to the appropriate schema, it's fairly straightforward to edit as VS will tell you the valid nodes as you type (shown below).

    • two .cs files containing method stubs for event handlers. These are ready for you to your implementation to.

    • schema.xml - this contains the CAML which dictates which content types the list stores, any custom views and also has references to the .aspx files we mentioned
  5. If you want to add default items to your list (and you probably do if you created an instance of it), you can edit the instance.xml file. This is made simple because VSeWSS has hooked up instance.xml to the appropriate XML schema (C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\XML\wss.xsd), and you can then take advantage of VS's Intellisense which is inferred from the schema as you type:-



  6. After making any customisations such as editing instance.xml to add list items, go to the properties of your VS project and examine the 'SharePoint Solution' tab. Here you'll see VSeWSS has generated two features for each list. One is for the list instance, the other is for the list template. This can be used by users to create new lists on the site, using the columns you've defined.

  7. Go to the 'Debug' tab on the project properties, and enter the URL of your SharePoint development site. The solution will be deployed to this URL so it's important this value is entered.

  8. Hit F5 and WSeWSS will create and deploy the solution. Note that the .wsp file gets generated in the bin\Debug or bin\Release directory of your project. Keep an eye on the VS status bar in the bottom left to monitor progress. If the deploy succeeded, your list should now be visible in the site specified on the 'Debug' tab.


To anyone who has created SharePoint solutions/features by hand, VSeWSS offers a compelling alternative. No messing around with ddf files and makecab.exe, just hit F5. However, there are a couple of irritations:-

  • Since VSeWSS rewrites the feature.xml file on each deploy, there's no way to customise this file to add something which can't be entered on the 'SharePoint Solutions' area of the project properties. The major example is a feature receiver class to specify code which should run when the feature is activated/deactivated, should you want to do something here. Of course, you could edit the file at the end of the development cycle, but you know damn well there's always further tweaks and re-pasting the XML into feature.xml gets tiresome.

  • VSeWSS recreates the feature GUIDs on each deployment. This is great for development, but means you cannot have other features with feature dependencies on your list features. This can be a pain if you're trying to create a feature hierarchy or rationalise your features so that activating one will activate all required features for a given part of your solution.

Nevertheless, you now have your solution with a feature for each list you added. Well actually you have 2 features for each list you added. Why? Because one is for the list instance and the other is for the list template. Note that you can deactivate the feature for the list template if you don't want your users to be able to create lists from this one.


Next in the series - how to create site columns (fields) which get their data from lists.

Tuesday, 27 February 2007

Series : how to create common MOSS site artifacts as features

Having been through the process of creating a MOSS site entirely through Features, it struck me how non-intuitive some of the associated tasks are. Many SharePoint developers who are working in this way will agree that something that takes 30 seconds in the UI can take several hours in Visual Studio. Over the next few articles, I'll note some of the issues I came across in this process, and also the solution I used for each.

But first, let's be clear on why developing SharePoint Features is the way to go for MOSS development. The main reasons as I see them are:-


  • ability to deploy updates through dev/QA/staging/production etc. in a repeatable manner (i.e. without recreating things manually through the UI - since this wouldn't scale well across many environments)
  • ability to deploy updates to all the web-front-end machines in the farm in one go (when the Feature is wrapped in a Solution - more on this later). Again, this avoids config management issues due to human error.
  • ability to properly source control the artifacts which comprise your MOSS site/application
  • to have a good foundation for Continuous Integration. Many shops would want to include their MOSS development in their automated build routines, and having the functionality created/deployed as Features facilitates this.
  • option of doing basic config management through the UI. Whilst most SharePoint devs/administrators would be comfortable writing STSADM deployment scripts, it's useful that less technical people can deploy/rollback changes if required. Obviously, most deployments would want to consider appropriate permissions for this ;-)
  • ability to have custom functionality automatically be included in the sites created by end users.

Andrew Connell's article on the benefits of deploying in this way is definitely worthwhile reading.

The next articles in my series will be:-

So long for now..

Chris.