Wednesday, April 30, 2014

Creating Document Set Based Containers

SharePoint 2010 and 2013 provide a content type called a DocumentSet. To add this content type to a site collection you must activate the feature “Document Sets”, or by code using FeatureID “3bae86a2-776d-499d-9db8-fa4cdc7884f8”. A document set is like a folder in a library, in that you can create or upload documents under the document set. It is also similar to a SPListItem because the Document Set can have custom site columns assigned to it.

For example, you could create a new content type based on the Document Set content type for presentations; maybe call it “Presentation”. You might create custom site columns such as “Presentation Date”, “Presenter Name”, “Presenter Email”, “Presentation Summary”, etc. and add them to the “Presentation” content type and it’s Welcome Page.

Then you can create new Presentation document sets with the Title, Presentation Date, Presenter Name, etc. The presentation materials, such as; slides, pictures, agenda, etc. can then all be uploaded and stored within this presentation document set as a related set of documents.

This post is about how to create a new document set container from a document set based content type, set its properties and add files to the document set. I do not cover the creation of Document Set based content types or the customization in this post.

PowerShell (sample code)
# Make sure the SharePoint namespace is loaded
add-pssnapin Microsoft.SharePoint.PowerShell –erroraction silentlycontinue;

# Set some sample variables
$weburl = "http://contoso.local/sites/Classes/USHistory";
$libraryname = "Materials";
$presentationtitle = "Civil War Weapons";
$files = ("E:\Civil War Weapons\Weapons.pptx", "E:\Civil War Weapons\Syllabus.pdf");

# Get the SPWeb object
$web = get-spweb $weburl;

# Get the content id of the presentation document set
$ctId = $web.contenttypes["Presentation"].id.tostring();

# Get the SPList object
$list = $web.lists.trygetlist($libraryname);
if ($list –eq $null) {
    # List not found, do error processing
    throw new-object ArgumentNullException($libraryname, "Library was not found!")
}
$folder = $list.rootfolder;

# Create a hash table for the custom property values
$hash = $null;
$hash = @{};
$hash.add("Presentation Date", get-date "5/12/2014 11:00 AM");
$hash.add("Presentater Name", "Joseph Assif");
$hash.add("Presentater Email", "jassif@university.edu");
$hash.add("Presentation Summary", "Lecture on the various weapons that were used during the US Civil War");

# Create the new presentation document set container
$newPresentation = [Microsoft.Office.DocumentManagement.DocumentSets.DocumentSet]::Create($list.rootfolder, $presentationtitle, $list.contenttypes.bestmatch($ctId), $hash);
$newPresentation.provision();

# If needed, you can capture the id
$newId = $newPresentation.item.id;

# Process the files for the document set
foreach ($file in $files) {

    # Read the file
    $f = get-childitem $file;
    $filestream = ([System.IO.FileInfo] (get-item $f.fullname)).openread();

    # Create the file in the document set
    $target = "{0}/{1}/{2}" –f $folder, $presentationtitle, $f.name;
    [Microsoft.SharePoint.SPFile]$spFile = $folder.files.add($target, [System.IO.Stream]$filestream, $true);
    $filestream.close();

    # Set any file properties you may have defined
    $spFile.item["Title"] = "Your document title text");
    $spFile.item.update();

    # Other document processes needed (checkin, etc.)

} # end foreach file

# Release SPWeb from memory
$web.dispose();


Friday, January 10, 2014

Content Organizer Rules

I had the need recently to create some content organizer rules that would route content types with specific metadata field values to specific folders within a library. To complicate matters, the metadata field value contained a taxonomy value.

I believe you cannot create such a rule via the SharePoint UI therefore I resorted to PowerShell. Let me break down how I accomplished this.
Before I do that let me outline the user background and requirement. The target library has folders within it, the primary purpose of the folders is to enable permissions granularity at the folder level. We created a new content type, named “HR Request”. This content type has a metadata field named “Request Sensitivity” which is a required selectable field of taxonomy term values like; “Corporate Executives”, “Human Resources”, “Managers”. Based on the selected value of this Sensitivity field we will route the HR Request to the folder with the correct permissions settings (i.e. we named the folders Corporate_Executives, Human_Resources, and Managers).

Let’s tackle the hardest part of this first, and that is creating the conditions string for the rule. We’ll need to have information about our field (“Request Sensitivity”) and our taxonomy term including its WssId to build the ConditionsString for our rule.

PowerShell (build a conditions string for a rule)
# Make sure the SharePoint namespace is loaded
Add-pssnapin Microsoft.SharePoint.PowerShell –erroraction silentlycontinue;

# Set some sample variables
$yourservername = "contoso.local";
$yoursitepath = "sites/Human Resources";
$yoursubweb = "Employees";

# Get an SPWeb object for the root and the subweb
$rootweb = get-spweb ("http://{0}/{1}" –f $yourservername, $yoursitepath);
$web = get-spweb ("http://{0}/{1}/{2}" –f $yourservername, $yoursitepath, $yoursubweb);

# Get our field and build the column portion of our conditions string
$field = $rootweb.Fields["Request Sensitivity"];
$column = "{0}|{1}|{2}" –f $field.Id, $field.InternalName, $field.Title;

# Get our taxonomy term
$taxSession = get-sptaxonomysession –site $rootweb.url;
$taxStore = $taxSession.TermStores[0];
$taxGroup = $taxStore.Groups["Human Resources"];
$taxTermSet = $taxGroup.TermSets["Request Sensitivity"];
$taxTerm = $taxTermSet.Terms["Corporate Executives"];

# Get the WssId for this term by querying the taxonomy hidden list
$spQuery = new-object Microsoft.SharePoint.SPQuery;
$spQuery.query = "<Where>";
$spQuery.query += "  <Eq>";
$spQuery.query += "    <FieldRef Name=’IdForTerm’ />";
$spQuery.query += "    <Value Type=’Text’>{0}</Value>" –f $taxTerm.Id;
$spQuery.query += "  </Eq>";
$spQuery.query += "</Where>";

$spList = $rootweb.lists.trygetlist("TaxonomyHiddenList");

$queryItems = $spList.getitems($spQuery);

if ($queryItems.count –gt 0) {
    $wssId = $queryItems[0]["ID"]; # If found, use it
}
else {
    $wssID = "-1";                 # Otherwise, use -1
}

# Now we can build the value portion of our conditions string
$value = "{0};#{1}|{2}" –f $wssId, $taxTerm.Name, $taxTerm.Id;

# Finally we can build the conditions string
$conditions = "<Conditions><Condition Column="{0}" Operator="IsEqual" Value="{1}" /></Conditions>" –f $column, $value;

Now that we have our conditions string built, let’s create our Organizer rule in PowerShell.

PowerShell (create a content organizer rule)
# Create an EcmDocumentRouterRule object
[Microsoft.Office.RecordsManagement.RecordsRepository.EcmDocumentRouterRule]$rule = new-object Microsoft.Office.RecordsManagement.RecordsRepository.EcmDocumentRouterRule($web);

# Set the properties of the rule
    # Set the content type the rule will apply to
    $rule.ContentTypeString = "HR Request";
    # Give the rule a name
    $rule.Name = "HR Request: Corporate Executives";
    # Give the rule some descriptive text
    $rule.Description = "Routes HR Requests with Request Sensitivity Corporate Executives to the Corporate_Executive folder.";
    # Give the rule a target path to the folder
    # The path can also be a relative server string such as:
    #    /sites/SiteName/ListTitle/FolderTitle
    $rule.TargetPath = $rootweb.SubFolders["Corporate_Executives"];
    # Since we’re not routing externally, set to false
    $rule.RouteToExternalLocation = $false;
    # Set the relative priority for the rule, in this case we want this rule to be evaluated first so we’ll use priority 3
    $rule.Priority = "3";
    # Enable the rule
    $rule.Enabled = $true;
    # Set the conditions for this rule (this is the complex part, see above for the steps to create the $conditions string
    $rule.ConditionsString = $conditions;

# Save the rule
$rule.Update();

# Release SPWebs from memory
$web.dispose();
$rootweb.dispose();

Now you say, what about a rule for “Human Resources” and “Managers”? Well, the process would be very similar for Human Resources. You would even use the same priority (eg. “3”). In our case, “Managers” is the default, so it does not need a fancy conditions string, just use “<Conditions></Conditions>” and skip all the steps I outlined above to build the conditions string, give it a lower priority (eg. “6”) and it will catch all HR Requests that are not routed by the higher priority rules.


Like so many things in SharePoint, it’s just a simple 106 step process!

Thursday, December 12, 2013

SharePoint Web Properties

I like to make use of web properties to make my solutions more flexible and configurable. For instance, a client of mine likes to request automated emails from SharePoint. But experience has taught me that these emails can get overwhelming and it’s not uncommon for a customer to come back later and request that I stop the emails or reduce their frequency. If the sending and frequency of emails is hard coded into a deployed solution, then the process to change the emails involves a code change and a redeployment which takes time and always has an element of risk to it. Therefore, in cases like these I will code my solution to read web properties to drive these behaviors.

For example, I might create a web property named “Customer_SendEmails_OnOff” which can have a value of “On” or “Off”, and another one named “Customer_Email_FrequencyHours” which would have a value of the number of hours between emails, like “6”. My deployed solution reads in these property values and behaves accordingly.  If it reads the “Customer_SendEmail_OnOff” property and the value is “Off” then it sends no emails, if it is “On” then it reads the “Customer_Email_FrequencyHours” property value to determine the minimum number of hours between emails.  This way, I can use a few PowerShell commands to alter these property values and correspondingly change the behavior of my deployed solution immediately.

Here are some useful code samples related to this process:

PowerShell (add or update a web property)
# Make sure the SharePoint namespace is loaded
Add-pssnapin Microsoft.SharePoint.PowerShell –erroraction silentlycontinue;

# Set some sample variables
$yourservername = "contoso.local";
$yoursitepath = "sites/Accounting/Audits";
$yourpropname = "Customer_Email_OnOff";
$yourpropvalue = "On";

# Get an SPWeb object
$web = get-spweb ("http://{0}/{1}" –f $yourservername, $yoursitepath);

# Get a hashtable of all the properties on the SPWeb
$props = $web.AllProperties;

# Add or update the property
# NOTE: Add "" to ensure PowerShell interprets value as a string
if ($props.ContainsKey($yourpropname)
{
    $props[$yourpropname] = "" + $yourpropvalue;
}
else
{
    $props.Add($yourpropname, "" + $yourpropvalue);
}

# Commit updates to the SPWeb
$web.update();

# Release SPWeb from memory
$web.dispose();

PowerShell (iterate all existing properties)
# Make sure the SharePoint namespace is loaded
Add-pssnapin Microsoft.SharePoint.PowerShell –erroraction silentlycontinue;

# Set some sample variables
$yourservername = "contoso.local";
$yoursitepath = "sites/Accounting/Audits";

# Get an SPWeb object
$web = get-spweb ("http://{0}/{1}" –f $yourservername, $yoursitepath);

# Get a hashtable of all the properties on the SPWeb
$props = $web.AllProperties;

# Iterate the property names and values
foreach ($p in $props.Keys)
{
    write-host –f green "PropertyName: " -nonewline;
    write-host –f yellow $p;
    write-host –f green "       Value: " -nonewline;
    write-host –f yellow $props[$p];
    write-host;
}

# Release SPWeb from memory
$web.dispose();

PowerShell (cleanup/delete selected existing properties, full script)
param(
    [Parameter(Mandatory=$true,HelpMessage='Target subweb for properties (e.g. http://contoso.local/sites/accounting/audits)')][System.String]$weburl)

# Get our web object
$web = get-spweb $weburl;

# Create an empty array to hold the keys to be deleted
$deleteKeys = @();

# Iterate through each property
$props = [System.Collections.HashTable]$web.allproperties;
foreach ($p in $props.keys)
{
    write-host -f green " PropertyName: " -nonewline;
    write-host -f yellow $p -nonewline;
    write-host -f red " <- Delete? " -nonewline;
   
    # Read key input until "Y" or "N" pressed
    do {
        $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown");
    }
    until (($key.Character -eq "Y") -or ($key.Character -eq "N"))
       
    # Echo the key pressed
    write-host -f White $key.Character;
       
    # Since we cannot update a collection we are iterating through,
    # add the keys to delete into our array
    if ($key.Character -eq "Y")
    {
        $deleteKeys += $p;
    }

}

# Delete the property keys that were marked for deletion
write-host;
foreach ($propkey in $deleteKeys)
{
    $web.allproperties.remove($propkey);
    write-host -f red "      Deleted: " -nonewline;
    write-host -f yellow $propkey;
}
write-host;

# Commit our changes
$web.update();

# Release the SPWeb object from memory
$web.dispose();

C# (read a property value)
// Set some sample variables
string yourservername = "contoso.local";
string yoursitepath = "sites/Accounting/Audits";
string yourpropname = "Customer_Email_OnOff";
string yourpropvalue = string.Empty();
object property = null;

// Access the site object
using (SPSite site = new SPSite(string.Format("http://{0}/{1}", yourservername, yoursitepath)))
{

    // Access the web object
    using (SPWeb web = site.OpenWeb())
    {

        // Attempt to get the property
        property = web.AllProperties[yourpropname];

        // Check for null
        if (property != null)
        {

            // Populate your variable with the value that was read
            yourpropvalue = property.ToString();

        }
        else
        {

            // Handle error
            throw new ArgumentNullException(yourpropname, "Property returned null.");

        }

   } // end using web

} // end using site