Wednesday, April 16, 2008

SharePoint Folder, Sub-Folder and Content Type (part 2)

In the previous post we saw how to make a folder hierarchy with there own content type, their own view and their own "New" menu from the user interface. The interesting part of this method is it is easy to implement with only a few clics. But the bad side of this method is we have to do this Folder/New Button implementation everytime we create a folder.

One answer would be to make this implementation automatically each time we create a folder. It is possible with SPItemEventReceiver. But let's focus more on how to make this association from the SharePoint object model ?

Let's say I want to have this kind of structure:

  • Year-Month folder
    • Archive Folder (with meta-data)
      • Document (with meta-data)
      • History folder
        • List of history rows (with meta-data)

For example:

  • 2008-04
    • PO12345684
      • 12345678.pdf
      • History as of 2008-04-01
        • history row 1
        • history row 2
        • history row 3
  • 2008-03
    • PO10000684
      • 10000684.pdf
      • History as of 2008-03-01
        • history row 1
        • history row 2
        • history row 3

I have an Archive List where I'll store all these data. Each folder or document will have its own set of meta-data. We will use a Content Type for each of them. These CT are supposed to be implemented and attached to the Archive list.. The archive folder name will be the Purchase Order Number (PONumber). The PO will be copied from another list, Purchase Orders.

Here is the code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;

namespace APichot.Services
{

    public class ArchiveData
    {
        //List and Content type names
        //These names will be used to retrieve the content type in the Archive List
        protected string _ArchiveList = "Archive"; //Name of the archive list
        protected string _FolderYM = "Year-Month Folder";
        protected string _FolderCT = "Archive Folder";
        protected string _FolderHCT = "History Folder";
        protected string _HistoryCT = "History Data";
        protected string _purchOrderCT = "Purchase Order Archive";
        protected string _docCT = "Document";
        //Helper for various processes
        protected HelperClass hc;

        protected SPListItem _item;
        protected string _PONumber = string.Empty;
        protected SPList _LstArchive;
        protected string _RecordingDate = string.Empty;

        //Size of the fake document for history
        protected byte[] _docSize = new byte[1];
        //Name of the fake document
        protected Guid _guid = Guid.NewGuid();
        protected int _increment = 0;

        /// <summary>
        /// Get the current item to be moved to the archive list
        /// </summary>
        /// <param name="item">PO Item</param>
        public ArchiveData(SPListItem item) {
            try {
                if (item == null) {
                    throw new ArgumentNullException("item", "This value cannot be null");
                }
                this._item = item;
                this._LstArchive = this._item.Web.Lists[this._ArchiveList]; //this._item.Web.Lists[this._ArchiveList];
                this._PONumber = this._item["PONumber"].ToString();
                if (string.IsNullOrEmpty(this._PONumber)) {
                    throw new Exception("PONumber cannot be null or empty");
                }
            }
            catch (Exception x) {
                Utilities.logMessage("Event Source", "Archive Data", "Constructor", x.ToString(), EventLogEntryType.Error, this._PONumber);
            }
        }
        /// <summary>
        /// Main method to save data to archive
        /// Run the savePO() and saveHistory() methods
        /// </summary>
        /// <param name="wp">Current Workflow properties</param>
        /// <param name="hd">All history data</param>
        /// <returns></returns>
        public bool saveToArchive(HelperClass hc) {
            if (hc == null) {
                throw new ArgumentNullException("hc", "This value cannot be null");
            }
            try {
                this.hc = hc;
                SPFolder ArchiveFolder = this.savePO();
                if (ArchiveFolder != null && ArchiveFolder is SPFolder) {
                    this.saveHistory(ArchiveFolder);
                }
                else {
                    throw new Exception("ArchiveFolder is null or is not an SPFolder");
                }
                return true;
            }
            catch (Exception x) {
                this.hc.logMessage("saveToArchive", x.ToString(), EventLogEntryType.Error);
                return false;
            }
        }
        /// <summary>
        /// Save the PO to the Archive list
        /// </summary>
        /// <returns>Return the archive folder generated to be used by the history folder as a parent folder</returns>
        protected SPFolder savePO() {
            string whereIAm = string.Empty;
            try {
                //Specify the content type to be attached to a folder
                SPContentType FolderYM = this._LstArchive.ContentTypes[this._FolderYM];
                SPContentType Folder = this._LstArchive.ContentTypes[this._FolderCT];
                SPContentType FolderH = this._LstArchive.ContentTypes[this._FolderHCT];
                SPContentType eMailFolder = this._LstArchive.ContentTypes[this._eMailFolderCT];
                SPContentType purchOrder = this._LstArchive.ContentTypes[this._purchOrderCT];
                SPContentType doc = this._LstArchive.ContentTypes[this._docCT];

                List<SPContentType> contentTypes = new List<SPContentType>();

                //Get the item creation date, wich will become the Year/Month folder
                string currentMonthFolder = this.buildMonthFolderName();
                SPFolder MonthFolder;
                //If the folder already exists, get it, else create it
                whereIAm = "Month Folder";
                try {
                    MonthFolder = this._LstArchive.RootFolder.SubFolders[currentMonthFolder];
                }
                catch {
                    this.hc.logMessage("savMonthFolder", "Month folder creation (" + currentMonthFolder + ")", EventLogEntryType.Information);
                    //Create the folder
                    MonthFolder = this._LstArchive.RootFolder.SubFolders.Add(currentMonthFolder);
                    //What goes into the New menu
                    contentTypes.Add(Folder);
                    //Associate the content type to the folder
                    MonthFolder.UniqueContentTypeOrder = contentTypes;
                    //Use the Properties Hashtable to store folder's meta-data 
                    MonthFolder.Properties.Add("Title", currentMonthFolder);
                    MonthFolder.Properties.Add("ContentTypeId", FolderYM.Id.ToString()); // Fix the content type for the folder
                    whereIAm = "Month Folder Update";
                    MonthFolder.Item.Update();
                }

                SPFolder ArchiveFolder;
                whereIAm = "Archive Folder";
                try {
                    //If the folder doesn't exists, then go to Catch
                    ArchiveFolder = MonthFolder.SubFolders[this._PONumber];
                    //Because of international settings (french), the comma is interpreted as a thousand separator by the hastable.
                    //We have to convert it to a period.
                    string poAmount = this._item["POAmount"].ToString().Replace(",", ".");
                    ArchiveFolder.Properties["POAmount"] =  (this._item["POAmount"] == null) ? "0" : poAmount;
                    ArchiveFolder.Properties["VendorName"] =  (this._item["VendorName"] == null) ? string.Empty : this._item["VendorName"].ToString();
                    ArchiveFolder.Properties["POStatus"] =  this._item["POStatus"].ToString();
                    this.hc.logMessage("savArchive Folder", "Archive folder updated", EventLogEntryType.Information);
                }
                catch {
                    //Folder 
                    //Process all about the folder
                    contentTypes = new List<SPContentType>();
                    //What goes into the new menu
                    contentTypes.Add(purchOrder);
                    contentTypes.Add(doc);
                    contentTypes.Add(FolderH);
                    contentTypes.Add(eMailFolder);

                    ArchiveFolder = MonthFolder.SubFolders.Add(this._PONumber);

                    ArchiveFolder.UniqueContentTypeOrder = contentTypes;
                    //Process all about the folder item
                    ArchiveFolder.Properties.Add("Title", this._PONumber);
                    ArchiveFolder.Properties.Add("PONumber", this._PONumber);
                    //Because Properties accepts only int, string and date, and not Double, we must change PO Amount to a string
                    //And transform the comma (decimal) into a period
                    string poAmount = this._item["POAmount"].ToString().Replace(",", ".");
                    ArchiveFolder.Properties.Add("POAmount", (this._item["POAmount"] == null) ? "0" : poAmount);
                    ArchiveFolder.Properties.Add("VendorName", (this._item["VendorName"] == null) ? string.Empty : this._item["VendorName"].ToString());
                    ArchiveFolder.Properties.Add("POStatus", this._item["POStatus"].ToString());
                    ArchiveFolder.Properties.Add("ContentTypeId", Folder.Id.ToString()); // Fix the content type for the folder
                }
                whereIAm = "Archive Folder Update";
                ArchiveFolder.Update();
                //Store a recording date for the History folder name
                this._RecordingDate = DateTime.Now.ToShortDateString().Replace("/", "-") + " " + DateTime.Now.ToShortTimeString().Replace(":", "h");
                //Copy the PO into the new folder
                whereIAm = "Copy file";
                string destUrl = this._item.Web.Site.Url + this._LstArchive.ParentWebUrl + ArchiveFolder.Url + "/" + this.buildName("PO") + ".pdf";
                //Copy the document from Purchase Order list to the Archive list, with meta-data
                this._item.File.CopyTo(destUrl);
                return ArchiveFolder;
            }
            catch (Exception x) {
                this.hc.logMessage("savePO" + whereIAm, x.ToString(), EventLogEntryType.Error);
                return null;
            }
        }
        protected void saveHistory(SPFolder ArchiveFolder) {
            try {
                //Create History Folder
                string hFolderName = this.buildName("History"); 
                SPFolder HistoryFolder = ArchiveFolder.SubFolders.Add(hFolderName);
                List<SPContentType> contentTypes = new List<SPContentType>();
                //CT for the folder
                SPContentType historyFolderCT = this._LstArchive.ContentTypes[this._FolderHCT];
                //CT for the files inside the folder
                SPContentType historyCT = this._LstArchive.ContentTypes[this._HistoryCT];
                //In this folder, we will be able to create only History Data
                contentTypes.Add(historyCT);

                HistoryFolder.UniqueContentTypeOrder = contentTypes;
                HistoryFolder.Properties.Add("Title", hFolderName);
                HistoryFolder.Properties.Add("ContentTypeId", historyFolderCT.Id.ToString());
                HistoryFolder.Update();

                historyFolderCT = null;
                //Store history rows
                this.storeHistoryRows(HistoryFolder, historyCT);
            }
            catch (Exception x) {
                this.hc.logMessage("saveHistory", x.ToString(), EventLogEntryType.Error);
            }
        }
        protected virtual void storeHistoryRows(SPFolder HistoryFolder, SPContentType historyCT) {
            try {
                //Reset the increment number for the UniqueFileName
                this._increment = 0;
                foreach (historyData hdata in this.hc.HistoryDataCollection.ListOfHistoryData) {
                    //Prepare a fake file for storing each history line as in a custom list. Manadatory for a docLib
                    //hData.FillItem methos is:
                    // public Hashtable FillItem(string CT_Id) {
                    //    Hashtable result = new Hashtable();
                    //    result.Add("Title",_PONumber); 
                    //    result.Add("PONumber",_PONumber);
                    //    result.Add("HistoryDescription",_historyDescription);
                    //    result.Add("HistoryOutcome",_historyOutcome);
                    //    result.Add("WFStart",_eventDate);
                    //    result.Add("ApproversName",_approver);
                    //    result.Add("ContentTypeId", CT_Id);
                    //    return result;
                    //}
                    SPFile fileH = HistoryFolder.Files.Add(this.getUniqueFileName(), this._docSize, hdata.FillItem(historyCT.Id.ToString()));
                    fileH = null;
                }
            }
            catch (Exception x) {
                this.hc.logMessage("storeHistoryRows", x.ToString(), EventLogEntryType.Error);
            }
        }

        protected string buildName(string s) {
            return this._PONumber + " " + s.Trim() + " " + this._RecordingDate;
        }
        protected string getUniqueFileName() {
            if (this._increment == 0) {
                this._guid = Guid.NewGuid();
            }
            this._increment++;
            return string.Format(@"{0}-{1:00}.txt", this._guid.ToString(), this._increment);
        }
        protected string buildMonthFolderName() {
            //Get the item creation date, wich will become the Year/Month folder
            DateTime itemCreate = (DateTime) this._item["Created"];
            return string.Format("{0} - {1:00}", itemCreate.Year, itemCreate.Month);
        }
    }
}

As you can see there are some tricky things:

  • For the New button, you have to prepare a List<> with SPContentType and add this list to the folder's UniqueContentTypeOrder property.
  • For setting meta-data to a folder it is actually worth to use the Folder's Properties property (see this post)
  • For inserting a file, it is actually worth to use a Hastable for meta-data associated with SPFile object (see this post again)

Try to avoid as possible the number of updates you make in order to insert data faster. I've explained it in this post

I've spent some time with this, I hope this helps.