Deploying Custom Document Type Icons to your Farm

Deploying custom icons, like the PDF one, can be an annoying task if your farm has more than 2 servers. Even when you deploy a whole new farm, or add a new server, you have to dive into your hive folder again, edit your DOCICON.XML file and copy/paste that icon into your images folder.

When thinking about it, this could be easily automated.

The requirements are quite simple:

  1. Store the icon somewhere centrally
  2. Keep track of which extension it belongs to
  3. Periodically check for new servers to deploy the custom icons to

A basic fill in:

  1. A custom list residing on the Central Administration
  2. A custom column with a text field for instance where the extension can be filled in
  3. A timer job which will check for changes and apply if needed

In a quick scheme it looks like the following when you deploy the solution:

icon deployment

The code should execute on every server in the farm. Why not on just one server? Because we need to edit a file in the hive on every specific server. The least privilege security model does not allow direct access to other servers’ 14 hive. Not only should it execute just once, it should check everyday if the entry is there, and if not, because it’s a new server, add the correct line in DOCICON.XML. This is where a custom timer job comes into the scene. We want the timer job to execute on every server, not just webfrontends, so the timer job will inherit SPServiceJobDefinition. An example timer job class looks like this:

[Guid("some ugly guid goes here xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx")]
class InstallIcon : SPServiceJobDefinition
{
    public InstallIcon()
        : base()
    { }

    public InstallIcon(string name, SPService service)
        : base(name, service)
    { }

    public override string DisplayName
    {
        get
        {
            return "Configure extra document icons";
        }
    }

    public override string Description
    {
        get
        {
            return "Configures extra icons to be displayed for non standard files.";
        }
    }

    public override void Execute(SPJobState jobState)
    {
        //implement code here
    }
}

In order to register the timer job on every server, we’ll add our custom job to the timer service; Hence the SPServiceJobDefinition inheritance. Create a new farm feature and add a feature receiver. Add the following FeatureActivated block:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    // make this a global const
    string timerJobName = "install-icon-job";
    bool exists = false;
    foreach (SPJobDefinition job in SPFarm.Local.TimerService.JobDefinitions)
    {
        if (job.Name == timerJobName)
        {
            exists = true;
            break;
        }
    }
    if (!exists)
    {
        InstallIcon timerJob = new InstallIcon(timerJobName, SPFarm.Local.TimerService);
        SPDailySchedule sched = new SPDailySchedule();
        sched.BeginHour = 0;
        sched.EndHour = 1;
        timerJob.Schedule = sched;
        timerJob.Update();
        timerJob.RunNow();
    }
}

Add the following FeatureDeactivating block:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    // make this a global const
    string timerJobName = "install-icon-job";
    foreach (SPJobDefinition job in SPFarm.Local.TimerService.JobDefinitions)
    {
        if (job.Name == timerJobName)
        {
            job.Delete();
            break;
        }
    }
}

Now the timer job gets registered and unregistered with the timer service based on the feature’s state. In order to place the icon correctly in your hive you need to implement the following logic:

  1. Get the path of the DOCICON.XML
  2. Edit the DOCICON.XML with the correct key/value pair
  3. Save
  4. Copy the image file to the correct directory

But before we do that, we’ll create the picture library on the central administration site collection. I’m doing this outside of the solution, but you can always implement it using code or XML so you have everything in your package.

picture library custom column AssociatedExtension

It’s a standard picture library with the name “FileIcons”, and I’ve added an extra column called “AssociatedExtension” which I use later on to associate the picture with a certain extension.

Now it’s time to implement the logic into our timer job’s execute method. Warning: the following codeblock is a wall of text. In a production environment, it’s best to split this up into different classes / methods. But for this post I decided to put it all into one code block so you can easily see the steps that get processed.

// Get the DOCICON.XML path
string docIconPath = SPUtility.GetGenericSetupPath(@"TEMPLATE\XML\DOCICON.XML");
XmlDocument docIconXML = new XmlDocument();
// Read it into an editable XML object
using (XmlReader reader = XmlReader.Create(docIconPath))
    docIconXML.Load(reader);

XmlNode byExtension = docIconXML.SelectSingleNode("/DocIcons/ByExtension");
XmlNodeList mappingList = docIconXML.GetElementsByTagName("Mapping");

// Get the picture library on the central administration
SPAdministrationWebApplication caWebApp = SPAdministrationWebApplication.Local;
SPSite oSite = caWebApp.Sites[0];
SPWeb oWeb = oSite.OpenWeb();
SPPictureLibrary oPictureLibrary = (SPPictureLibrary)oWeb.Lists["FileIcons"];

// For every picture in the library, write the image to IMAGES
foreach (SPListItem item in oPictureLibrary.Items)
{
    Byte[] fileContentsArray = null;
    MemoryStream imageStream = null;
    SPFile file = item.File;
    string fileName = file.Name;
    using (Stream fileContents = file.OpenBinaryStream())
    {
        long length = fileContents.Length;
        fileContentsArray = new Byte[length];
        fileContents.Read(fileContentsArray, 0, Convert.ToInt32(length));
    }
    imageStream = new MemoryStream(fileContentsArray);
    FileStream fStream = File.OpenWrite(SPUtility.GetGenericSetupPath(@"TEMPLATE\IMAGES\" + fileName));
    imageStream.WriteTo(fStream);
    fStream.Flush();
    fStream.Close();
    imageStream.Flush();
    imageStream.Close();

    // Replace existing entries in the DOCICON.XML
    bool exists = false;
    foreach (XmlNode mapping in mappingList)
    {
        if (mapping.Attributes["Key"] != null && mapping.Attributes["Key"].Value == item["AssociatedExtension"].ToString())
        {
            exists = true;
        }
    }
    if (!exists)
    {
        XmlElement pdfEntry = docIconXML.CreateElement("Mapping");

        byExtension.AppendChild(pdfEntry);

        XmlAttribute key = docIconXML.CreateAttribute("Key");
        XmlAttribute value = docIconXML.CreateAttribute("Value");

        key.Value = item["AssociatedExtension"].ToString();
        value.Value = fileName;

        pdfEntry.SetAttributeNode(key);
        pdfEntry.SetAttributeNode(value);
        docIconXML.Save(docIconPath);
    }
}
oWeb.Dispose();
oSite.Dispose();

When you deploy your solution to your farm, add some pictures to your picture library and run the timer job, you’ll see that the changes get applied to your local 14 hive!

For more refinement, you may implement deletion events and tweak some settings with application pages.