Most of the documentation I've seen out there around creating publishing items such as master pages and page layouts has developers using SharePoint Designer to create them. This is ok, but it only gets you so far, and I see problems with the approach when you require more robust development. Also, when it came to using SharePoint Designer, I always had the question - 'how do you develop a master page or page layout locally in development, and deploy it to a completely different farm?'
As my past couple of posts might point to, I've been working a lot with WSS Solutions lately, and I'm currently working with a client who's doing a migration from MCMS 2002 to MOSS. The solution is completely custom, and needs to be as robust as possible. Part of the project has been documenting the best possible configuration for MOSS CMS development, so I figured what I've found would be something good to share.
Essentially, there's two features in the solution. One feature for custom resource files (RESX) and another feature for all the custom publishing items. I'm using the full Telerik Editor for MOSS - which seems to have all the kinks worked out from the earlier versions - along with some other Telerik ASP.NET controls. I've heard that Andrew Connell is working with Telerik on releasing a whitepaper at the end of this month that will provide the needed guidance for leveraging best practices in implementing all of the Telerik ASP.NET components into MOSS and WSS v3, and I'll most likely touch on it in the future, as well.
I don't want to really cover the details of the MOSS resource feature in this post, but Mikhail Dikov has a great one on creating custom resource files in MOSS, so make sure to check it out. I will, however, give you what you need to get it up and running.
So - let's start from scratch!
Create a new Class Library project in Visual Studio 2005:
Once it's created, delete Class1.cs.
Add a reference to Windows SharePoint Services, Microsoft Content Publishing and Management, and System.Web.
I really like to mirror the installation directory on the server, so I normally set up a folder structure in my projects to match what the file structure on the server looks like, along with the folders that correspond with the locations of the files I'm going to add..
The first thing I'm going to do is create the resource feature along with the needed feature receivers to deploy the resource files to the App_GlobalResources directory of the web application it's installed on.
This is code I mainly obtained from Mikhail's post, but here's how you set up the resource feature:
If you set up the folder structure just like I did in the image above, create a class in the 'Objects' directory called DeployCustomResourceJob.cs, with the following code:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
using System.Diagnostics;
using System.IO;
namespace MOSSFeatures.Objects
{
public class DeployCustomResourcesJob : SPJobDefinition
{
#region Fields
[Persisted]
private string _SourcePath;
private string _SomethignElse;
#endregion
#region Constructors
public DeployCustomResourcesJob()
: base()
{
}
public DeployCustomResourcesJob(string jobName, SPWebApplication webApp, string featureName)
: base(jobName, webApp, null, SPJobLockType.Job)
{
//get the path to the feature directory
this._SourcePath = SPUtility.GetGenericSetupPath("Template");
//uncomment the following line (commented to fix a bug in livewriter)
// + "\\FEATURES\\" + featureName;
}
#endregion
#region Job Handling
public override void Execute(Guid targetInstanceId)
{
try
{
//get the web application for the current job - I'm pretty sure this is the same as calling 'base.WebApplication'
SPWebApplication webApp = this.Parent as SPWebApplication;
foreach (SPUrlZone zone in webApp.IisSettings.Keys)
{
//the settings of the IIS application to update
SPIisSettings settings = webApp.IisSettings[zone];
//determine the destination path
string destPath = Path.Combine(settings.Path.ToString(), "App_GlobalResources");
//get the RESX files from the feature directory
string[] filePaths = Directory.GetFiles(this._SourcePath, "*.resx");
//copy/overwrite the RESX files into the App_GlobalResource directory
foreach (string filePath in filePaths)
{
string fileName = Path.GetFileName(filePath);//get the filename of the file to copy
File.Copy(filePath, Path.Combine(destPath, fileName), true);//copy it over the file in the Resource directory
}
}
}
catch (Exception ex)
{
Debug.WriteLine("Failed to copy global resource");
Debug.WriteLine(ex);
throw;
}
}
#endregion
}
}
Next - create a class in the 'FeatureReceivers' directory called CustomFeatureReceiver.cs. This is the feature receiver which will move the resource files once the feature is activated. Here's the code:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using MOSSFeatures.Objects;
namespace MOSSFeatures.FeatureReceivers
{
public class CustomResourceReceiver : SPFeatureReceiver
{
#region Constants
const string JOB_TITLE = "Deploy Custom Resources";
const string JOB_NAME = "job-deploy-custom-resources";
#endregion
#region Events
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWeb web = properties.Feature.Parent as SPWeb;
//check for an exisiting instance of the job - if we find it, delete it
foreach (SPJobDefinition job in web.Site.WebApplication.JobDefinitions)
{
if (job.Name == JOB_NAME && job.WebApplication.Name == web.Site.WebApplication.Name)
{
job.Delete();
string something = "nothing";
}
}
//create a new deployment job
DeployCustomResourcesJob deployJob = new DeployCustomResourcesJob(JOB_NAME,
web.Site.WebApplication,
properties.Definition.DisplayName);
deployJob.Title = JOB_TITLE;
deployJob.Schedule = new SPOneTimeSchedule(DateTime.Now);
deployJob.Update();
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPWeb web = properties.Feature.Parent as SPWeb;
//delete the job, if there is one
foreach (SPJobDefinition job in web.Site.WebApplication.JobDefinitions)
{
if (job.Name == JOB_NAME && job.WebApplication.Name ==
web.Site.WebApplication.Name)
{
job.Delete();
}
}
//we could delete the resource files here if we want to
}
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
//throw new Exception("The method or operation is not implemented.");
}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
//throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}
Create two RESX files in the TEMPLATE\FEATURES\CustomResources project directory - CustomResources.en-US.resx, and CustomResources.resx. According to Mikhail, one resource file is used during the provisioning process, and the other is used at run-time, but the rule I use is that the entries in both resource files should match. These two files will contains contain the resource strings used in the custom publishing feature, as well as anywhere else you might need them.
Create the Feature.xml file for the CustomResources feature.
<Feature
Title="CustomResourceFeature"
Id="AC3CB30C-905F-433f-A635-DA13291BCC08"
Description="Deploys localization resources used for MOSSFeatures to the App_GlobalResources directory"
Version="1.0.0.0"
Scope="Site"
xmlns="http://schemas.microsoft.com/sharepoint/"
ReceiverAssembly="MOSSFeatures, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ENTER_YOUR_PUBLIC_KEY_HERE"
ReceiverClass="MOSSFeatures.FeatureReceivers.CustomResourceReceiver" AlwaysForceInstall="true" ActivateOnDefault="false">
<ElementManifests>
<ElementFile Location="CustomResources.en-US.resx"/>
<ElementFile Location="CustomResources.resx"/>
</ElementManifests>
</Feature>
There - that's all you need to implement custom resource files in MOSS. Now we need to create the WSS Solution file (WSP) to deploy them.
At the root of the project, create a new file called CreateMOSSFeatures_WSP.ddf. This ddf file is used by makecab so it knows where to place files inside the cabinet (WSP) file.
Enter the following code in the ddf file
.OPTION EXPLICIT ;generate errors
.Set CabinetNameTemplate=MOSSFeatures.wsp
.Set DiskDirectoryTemplate=CDROM ;All cabinets go into a single directory
.Set CompressionType=MSZIP ;** All files are compressed in cabinet files
.Set UniqueFiles="ON"
.Set Cabinet=on
.Set DiskDirectory1=
;Here's the way it works...
;FileToCopyFromInProject