Friday 26 October 2012

Change the content type of list items using PowerShell

If you've got a content type deployed in multiple lists in site, and you want a way to quickly change all the existing items to the new content type, you can use PowerShell to do the dirty work.

Note that there are a few caveats to this:
1. The default content type of the list isn't being changed by the script
2. If you change the content type of items, and the new content type has new fields that are require fields, users will be prompted to enter information into the required fields next time they edit the item.
3. You need to add the new content type to each list that will be updated, before running this script

Firstly, if you want to understand how many items are going to be updated, run this script, which will report the number of list items to be updated for each list.

List items that need changing:

$oldCtName = "theNewContentTypesName";
$newCtName = "theOldContentTypesName";
$w = get-spweb http://my;
$a = $null;
$a = @();
$lc = $w.Lists;
foreach($l in $lc){
$lT = $l.Title;
if($l.ContentTypesEnabled -eq $false){continue;}
if($l.IsContentTypeAllowed($contenttype) -eq $false){continue;}
$cT = $l.ContentTypes[$newCtName];
if($cT -eq $null){continue;}
$ic = 0;
$listitems = $l.Items;
$listname = $l.Title;
Write-Host "Found Content Type:"$cT.Name"on list $listname";
foreach($i in $listitems){
if($i["Content Type"] -eq $oldCtName){$ic++;}
};
if($ic -gt 0){
$a += ("List $listname has $ic items which have been updated with the new content type.")
};
}
$a


To update the content type for each list item (in each list in the web that has your new content type added to it), use the following script:

Change Content Type

$oldCtName = "theNewContentTypesName";
$newCtName = "theOldContentTypesName";
$w = get-spweb http://my;
$CtypeId = [Guid]("{03e45e84-1992-4d42-9116-26f756012634}") #SPBuiltInFieldId.ContentTypeId
$a = $null;
$a = @();
$lc = $w.Lists;
foreach($l in $lc){
$lT = $l.Title
if($l.ContentTypesEnabled -eq $false){
Write-Host "Content Types are not enabled on the $lT list";
continue;
}
if($l.IsContentTypeAllowed($contenttype) -eq $false){
Write-Host "Content Type not allow on the $lT list";
continue;
}
$cT = $l.ContentTypes[$newCtName];
if($cT -eq $null){
Write-Host "Content Type has not been added to this list. Skipping to the next list.";
continue;
}
Write-Host "Found Content Type:"$cT.Name;
$ic = 0;
$listitems = $l.Items
$listname = $l.Title;
foreach($i in $listitems){
if($i["Content Type"] -eq $oldCtName){
$i[$CtypeId] = $cT.Id;$i.SystemUpdate();$ic++;
}
};
if($ic -gt 0){
$a += ("List $listname has $ic items which have been updated with the new content type.")
};
}
$a

Thursday 25 October 2012

Getting a little more from SharePoints Web Analytics

I did this ages ago, but I thought I blog about it now, in case it helps someone.

SharePoint has a built-in web analytics webpart. It provides some interesting info out of the box, but I wanted a tailored version that would display the top pages based on the users department, and cache it, without the user having to select any options.

I developed a webpart, that uses the same SQL function SharePoint does to query SharePoint's analytics database, returning the most popular pages being accessed by people in the same department (I used SQL Analyzer to find out the function SharePoint was using).

The function, fn_WA_GetClickthroughChanges, takes 8 inputs, of which I'm supplying 6, and using the default for the other two, indicated by *:

The current date as an Int (todays date minus the number of days you want to trend over. I.e. -30)
The previous date as an Int (the "current date", as defined above, minus the number of days you want to trend over)
The number of days to trend across
The site id
A bit value that indicates whether to include sub sites
The content type (default is null)*
The users title (default is null)*
The users department

The webpart queries the database for the most accessed pages based on the current users department, gets the page title (and does some optional formating depending on the page name), and caches the output.

You can download the wsp/source code here:

Source code and WSP file

It looks like this:


The code looks like this:

private const string GetRecentTrend = "SELECT TOP (200) * FROM [dbo].[fn_WA_GetClickthroughChanges]({0}, {1}, {2}, '{3}', {4}, default, default, {5})  ORDER BY [CurrentFrequency] DESC";
private string GetMostPopularLinks()
{
 var cacheObjectName = String.Format("inceMostPopular{0}", GetUsersDepartment());
 var cachedOutput = HttpRuntime.Cache[cacheObjectName];
 if (cachedOutput != null)
 {
  return (String)cachedOutput;
 }

 SqlConnectionStringBuilder cs = new SqlConnectionStringBuilder();
 cs.UserID = SpAnalyticsSqlUser;
 cs.Password = SpAnalyticsSqlUserPassword;
 cs.DataSource = SpAnalyticsSqlServer;
 cs.InitialCatalog = SpAnalyticsSqlServerDatabase;
 SqlConnection conn = new SqlConnection(cs.ConnectionString);
 StringBuilder im = new StringBuilder();
 im.Append("<ul id=\"inceMPList\">");
 im.Append(String.Format("<li id=\"inceMPListFirstItem\">{0}</li>", Title));

 try
 {
  conn.Open();
  Int32 lastNumberOfDays = NumberOfDaysToLookBack;
  String siteId = SubstituteWebId == String.Empty ? SPContext.Current.Site.ID.ToString() : SubstituteWebId;
  String department = GetUsersDepartment();
  DateTime currentDate = DateTime.Now.AddDays(-lastNumberOfDays);
  DateTime previousDate = currentDate.AddDays(-lastNumberOfDays);
  Int32 currentDateAsInt = Int32.Parse(String.Format("{0}{1}{2}", currentDate.Year, currentDate.Month.ToString().PadLeft(2, '0'), currentDate.Day.ToString().PadLeft(2, '0')));
  Int32 previousDateAsInt = Int32.Parse(String.Format("{0}{1}{2}", previousDate.Year, previousDate.Month.ToString().PadLeft(2, '0'), previousDate.Day.ToString().PadLeft(2, '0')));

  SqlCommand command = new SqlCommand();
  command = department != String.Empty ? new SqlCommand(String.Format(GetRecentTrend, currentDateAsInt, previousDateAsInt, lastNumberOfDays, siteId, 1, String.Format("'{0}'", department)), conn) : new SqlCommand(String.Format(GetRecentTrend, currentDateAsInt, previousDateAsInt, lastNumberOfDays, siteId, 1, "default"), conn);

  command.CommandType = CommandType.Text;
  var dr = command.ExecuteReader();
  if (dr != null)
  {
   if (dr.HasRows)
   {
    String trendup = "<img src=\"/_layouts/images/ince/isqtrendup.png\" alt=\"trend up\"/>";
    String trenddown = "<img src=\"/_layouts/images/ince/isqtrenddown.png\" alt=\"trend up\"/>";
    Int32 currentRank = 1;
    Int32 rowCount = 0;
    Int32 maxCout = MaximumItemsToDisplay;

    while (dr.Read())
    {
     String pageId = dr["PageId"] == DBNull.Value ? String.Empty : (String)dr["PageId"];
     //|| pageId.ToLower().Contains("/lists/") || pageId.ToLower().Contains("/forms/")
     if (pageId.ToLower().EndsWith("home.aspx") || pageId.ToLower().EndsWith("default.aspx") || pageId.ToLower().Contains("_layouts") || pageId.ToLower().Contains("/searchcenter/"))
     {
      currentRank++;
      continue;
     }

     var oCurrentFrequency = dr["CurrentFrequency"];
     var oPreviousRank = dr["PreviousRank"];

     Int64 currentFrequency = oCurrentFrequency == DBNull.Value ? 0 : (Int64)oCurrentFrequency;
     Int64 previousRank = oPreviousRank == DBNull.Value ? 0 : (Int64)oPreviousRank;
     String pageName = GetPageTitle(pageId);

     if(currentRank > previousRank)
     {
      im.Append(String.Format("<li class=\"inceMPItem\"><a href=\"{0}\" alt=\"{1}\">{1}{2}</a></li>", pageId, String.Format("{0} ({1})", pageName, currentFrequency), trendup));  
     }
     if (currentRank == previousRank)
     {
      im.Append(String.Format("<li class=\"inceMPItem\"><a href=\"{0}\" alt=\"{1}\">{1}</a></li>", pageId, String.Format("{0} ({1})", pageName, currentFrequency)));
     }
     if (currentRank < previousRank)
     {
      im.Append(String.Format("<li class=\"inceMPItem\"><a href=\"{0}\" alt=\"{1}\">{1}{2}</a></li>", pageId, String.Format("{0} ({1})", pageName, currentFrequency), trenddown));
     }
     currentRank++;
     rowCount++;
     if(rowCount == maxCout)
     {
      break;
     }
    }
   }
   else
   {
    im.Append(String.Format("<li class=\"inceMPItem\"><span>No Results</span></li>"));
   }
   dr.Close();
  }
 }
 catch (Exception exception)
 {
  return String.Format("[GetMostPopularLinks] Unhandled Exception retrieving most popular links. Error: {0}",exception.Message);
 }
 finally
 {
  conn.Close();
 }
 im.Append("</ul>");
 HttpRuntime.Cache.Insert(cacheObjectName, im.ToString(), null, DateTime.UtcNow.AddMinutes(MinutesToCacheLookup), Cache.NoSlidingExpiration);
 return im.ToString();
}

private string GetPageTitle(string pageId)
{
 bool originalCatchValue = SPSecurity.CatchAccessDeniedException;
 SPSecurity.CatchAccessDeniedException = false;
 try
 {
  if(SubstituteWebHostHeader != String.Empty)
  {
   var currentWebHh = pageId.Substring(pageId.IndexOf("//")+2);
   currentWebHh = currentWebHh.Substring(0, currentWebHh.IndexOf("/"));
   pageId = pageId.Replace(currentWebHh, SubstituteWebHostHeader);  
  }
  using (SPSite spSite = new SPSite(SPContext.Current.Site.Url))
  {
   using (SPWeb spWeb = spSite.OpenWeb(pageId))
   {
    if (pageId.ToLower().Contains("/forms/") || pageId.ToLower().Contains("/lists/"))
    {
     if(pageId.ToLower().Contains("/lists/"))
     {
      var firstPart = pageId.Substring(0, pageId.ToLower().LastIndexOf("/"));
      firstPart = firstPart.ToLower().Replace("/lists/", "/");
      firstPart = firstPart.Substring(spSite.Url.Length);
      return firstPart;
     }
     if (pageId.ToLower().Contains("/forms/"))
     {
      var firstPart = pageId.Substring(0, pageId.ToLower().LastIndexOf("/"));
      firstPart = firstPart.ToLower().Replace("/forms", "/");
      firstPart = firstPart.Substring(spSite.Url.Length);
      return firstPart;
     }
    }
    var item = spWeb.GetListItem(pageId);
    if (item != null)
    {
     return item.Title == String.Empty ? pageId.Substring(pageId.LastIndexOf('/')) : item.Title;
    }
   }
  }
  return pageId.Substring(pageId.LastIndexOf('/'));
 }
 catch (Exception)
 {
  return pageId.Substring(pageId.LastIndexOf('/'));
 }
 finally
 {
  SPSecurity.CatchAccessDeniedException = originalCatchValue;
 }
}

Wednesday 17 October 2012

Hiding default actions on the ECB (Edit Control Block, or List Item Menu)

My requirement:

Remove the "Modify Permissions" action from the list item menu for lists that have a special content type (used in my solution). Since I need to implement a special application page for modifying list item permissions on the list my solution uses, I've added my own custom action for "Manage Permissions". I don't want to confuse users with two menu items for permissions, nor do I want them using the default "Modify Permissions" action, because it will break my solutions functionality.

While there are a number of ways to do this, most use jQuery and most wait for the ECB to be loaded before they remove the item. I don't particularly like this, because the user will typically see the "Modify Permissions" action for a brief moment before it's removed.

So I decided on a different approach that doesn't rely on jQuery. It uses JavaScript, CSS and leverages an object output by the List View Webpart, ctx, to hide the "Manage Permissions" action using CSS.

The solution elements:



Step 1: Create a user control, and define my JavaScript. The trick here is to limit the time wasted running the script on every page.

To ensure excessive processing isn't performed, the JavaScript will check to see if the ctx object has been defined (indicating the page has an instance of the List View Webpart on it).

If ctx has been defined, the script then checks the ctx.listTemplate property to see if the list instance is based on my solutions custom list template.

If the list template used is the one my solution has deployed, then it writes some CSS to the page to hide the "Manage Permissions" action on the ECB / LIM. The CSS output uses an attribute selector to hide list items with the attribute "sequence" set to 1160 (which is what "Manage Permissions" is set to; you can use Firefox and Firebug to find the sequence number for other controls).

All the user control contains is the following script:

<script type="text/javascript">
ExecuteOrDelayUntilScriptLoaded(editEcbMenusEx, 'Core.js');
function editEcbMenusEx() {
try {
if (typeof ctx == "undefined") {
return;
}
else {
if (ctx.listTemplate == 15501) {
var css = 'li[sequence="1160"]{display:none;}';
var head = document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet){ style.styleSheet.cssText = css; }
else { style.appendChild(document.createTextNode(css)); }
head.appendChild(style);
}
}
} catch (e) {
}
}
</script>


Step 2: Create a custom action (that gets deployed at the web level when my solution is activated) that loads my control into the AdditionalPageHead place holder of the webs master page.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Id="AdditionalPageHead" Sequence="150" 
  ControlSrc="~/_CONTROLTEMPLATES/inceriskregister/RRecbmodifications.ascx">
  </Control>
</Elements>


Step 3. Deploy the solution
[Note: The Manage Permissions action that you see on the LIM is the new custom action that I've defined, which opens my custom manage permissions application page in a modal dialog)




Display multiple nested modal dialogs boxes using a custom action

Adding custom actions to SharePoint's ribbon and context menus is rather cool (I reckon), as well as rather easy! However, while adding an item to the ECB (Edit Control Block) / LIM (List Item Menu) the other day, I was getting an annoying JavaScript issue when closing the dialogs.

Firstly, a little more on what I was doing. From the LIM, I had a menu item that opens an application page in a modal dialog box for creating a new item (it uses a special content type and also creates associated items, hence the special application page). When the user submits the form, the new list items are created, and a second form (the default edit form for the new parent list item) is opened in a new dialog box, allowing the user to enter more information into the fields of a child list item that was created.

So far, so good. When the user clicks Save on the edit form, the form should close (all modal dialogs) and the list (page) should be refreshed to show the new items created.

1. Click on the custom action, which opens the application page in the first modal dialog

2. Enter the new controls (new item) title, and click Save (which creates two new items, a "control item", and an "action item" (which inherits certain field values from the control item), and opens a new dialog containing the default edit page for the new "action item"

3. User fills in the fields for the new "action item" and clicks OK

4. Both modal dialogs should be closed, and the list (parent page) should be refreshed.

The problem was, when the edit form was submitted, the modal dialogs would close ok, but the page wouldn't be refreshed, and instead a JavaScript error was raised.

Message: Function expected
Line: 1139
Char: 13
Code: 0
URI: http://my/_layouts/sp.ui.dialog.debug.js?rev=I4RtkztzINg%2B%2BPCPe%2FeQlw%3D%3D

To cut a long, boring and frustrating story short, this all came down to how I was calling the first modal dialog and forgetting to define the callback function in the custom action.

To get this working correctly, this is what I did;

Put the JavaScript into the URL element of the custom action that will open my application page in a SharePoint modal dialog box (which is where I went wrong initially, forgetting to define the callback function, hence the JavaScript error):

<CustomAction Title="New Control" Id="{1ac99a52-2fdd-4217-b7f4-664011f45251}" 
ImageUrl="/_layouts/images/inceriskregister/document-new32.png" 
Location="EditControlBlock" RegistrationId="0x01007006E66EE8DD45458AEC3EF515A0E6E101" 
RegistrationType="ContentType" Sequence="2" xmlns="http://schemas.microsoft.com/sharepoint/" 
Description="Add a new control for this risk." Rights="AddListItems" >
 <UrlAction Url="javascript:function displayStatus(dialogResult, returnValue) 
 {SP.UI.ModalDialog.RefreshPage(1); SP.UI.ModalDialog.commonModalDialogClose(1,1); }; 
 OpenPopUpPageWithTitle('/_layouts/inceriskregister/newcontrol.aspx?parentId={ItemId}&amp;
 ListId={ListId}&amp;webUrl={SiteUrl}&amp;',displayStatus,null,null,'Add New Control')" 
 />
</CustomAction>


As for my application page, it has the following JavaScript in the page head to handle opening the second modal dialog (after the user has entered a title on the form and clicked Save), and for closing itself:

<script type="text/javascript">
function inceOpenDialog(listEditFormUrl, id, listid) {
ExecuteOrDelayUntilScriptLoaded(function () 
{ intInceOpenDialog(listEditFormUrl, id, listid); }, 'sp.js');}

function intInceOpenDialog(listEditFormUrl, id, listid) {
  try {
   var editForm = listEditFormUrl + '?ID=' + id + '&ListId=' + listid + "";
   var options = {
    url : editForm,
    autoSize : true,
    showClose : true,
    allowMaximize : false,
    dialogReturnValueCallback : displayStatus
   };
   SP.UI.ModalDialog.showModalDialog(options);
  } catch(e) {
   alert(e);
  }
 }

function displayStatus(dialogResult, returnValue) {
  SP.UI.ModalDialog.RefreshPage(1);
  SP.UI.ModalDialog.commonModalDialogClose(dialogResult, returnValue);
 }

function inceCloseModalDialog(refresh) {
  SP.UI.ModalDialog.RefreshPage(1);
  SP.UI.ModalDialog.commonModalDialogClose(dialogResult, returnValue);
 }  
</script>


It always helps if you do thing right the first time. Unfortunately it sometimes takes a few hours of rooting around and getting increasingly frustrated just to find that you've implemented something incorrectly!

No Exact Match Found (SharePoint's PeoplePicker control)

While testing an application page I'd just developed which had a few people picker controls, I noticed that I could select users, SharePoint groups and security groups (as desired), but when I clicked on the validate button, I got the message:

No exact match was found. Click the item(s) that did not resolve for more options.

If I ignored the validation error and clicked Save, the code behind worked fine, and the selected users and groups where process accordingly.





So why was I getting a validation error for groups that exist? Because I was setting the SelectionSet in code during the Page_Load event, the SelectionSet property was being over written when the control was loaded later in the page lifecycle (see: ASP.NET Page Life Cycle Overview)

ppAdmin.SelectionSet = String.Format("{0},{1},{2}", 
PeopleEditor.AccountType.User, 
PeopleEditor.AccountType.SecGroup, 
PeopleEditor.AccountType.SPGroup);

When I added the property to the declaration, presto, it worked properly!

<SharePoint:PeopleEditor runat="server" ID="ppAdmin" 
MultiSelect="True" Height="75" AllowEmpty="False" 
DialogWidth="495" Width="495" AllowTypeIn="True" 
SelectionSet="User,SecGroup,SPGroup" />



Using Multi-valued UserInfo fields (UserMulti)

Call me a dumb arse (maybe I was just tired at the time?!), but I had some issues the other day reading and writing users from a multi-valued UserInfo field.

I had a UserMulti type field declared in my solution, and needed to read and write to it via an application page.

The field declaration:

<Field ID="{084A686D-60CA-4E95-AB4D-55D8B1DF1602}" DisplayName="APTN" Name="ictRiskAPTN" StaticName="ictRiskAPTN" Group="Ince Risk"         List="UserInfo" ShowField="ImnName" Required="FALSE" Type="UserMulti" Description="By default... (omitted)" UserSelectionMode="PeopleOnly" UserSelectionScope="0" AllowMultiVote="TRUE" />

To read from the field:

[Note: Fields.AdditionalPeopleToNotify.Id is a property of a helper class that contains all the Ids, Displaynames and static names of my declared fields)]

var additionalRecipients = i[Fields.AdditionalPeopleToNotify.Id] as SPFieldUserValueCollection;
if (additionalRecipients != null)
{
if (additionalRecipients.Count > 0)
{
foreach (SPFieldUserValue uservalue in additionalRecipients)
{
//Do something with your user
}
}
}

To set the value of the field:

var i = listItems[0];
SPFieldUserValueCollection users = new SPFieldUserValueCollection();
SPPrincipal pr = web.EnsureUser("domain\\username") as SPPrincipal;
users.Add(new SPFieldUserValue(i.Web, pr.ID, pr.Name));
i[Fields.AdditionalPeopleToNotify.Id] = users;
i.SystemUpdate();

Or, for a more realistic example, set the value of the field based on a list of users or groups selected from a PeoplePicker control:

var i = listItems[0];
SPFieldUserValueCollection users = new SPFieldUserValueCollection();
if (peoplePicker.Entities.Count > 0)
{
foreach (PickerEntity entity in peoplePicker.Entities)
{
SPPrincipal pr = null;
if (entity.EntityData.ContainsKey("PrincipalType"))
{
var pT = entity.EntityData["PrincipalType"] as String;
if (pT != null)
{
switch (pT)
{
case "SharePointGroup":
pr = web.SiteGroups[entity.Key] as SPPrincipal;
break;
case "User":
pr = web.EnsureUser(entity.Key) as SPPrincipal;
break;
case "SecurityGroup":
pr = web.EnsureUser(entity.Key) as SPPrincipal;
break;
default:
break;
}
}
}
if (pr != null)
{
users.Add(new SPFieldUserValue(item.Web, pr.ID, pr.Name));
}
}
}
i[Fields.AdditionalPeopleToNotify.Id] = users;
i.SystemUpdate();

Inconsistent CSS styles applied to an email in Outlook when sending email from SharePoint

Recently I was working on a timer job that sends (html) emails via SharePoint's SPUtility.SendEmail function. The CSS styles I used in the email body were being inconsistently applied to the email when viewed in Outlook 2010. The method I was using was:

Create the email body, applying the styles at the top of the email body.
emailBody.Append(AddStyles());
emailBody.Append(body);

Send the email setting the fAppendHtmlTag parameter to true.
SPUtility.SendEmail(web, true, false, recipient, subject, emailBody)

To fix the issue, I set the fAppendHtmlTag parameter to false, instead adding the HTML tags myself, as such:
var body = String.Format("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"><html><head><meta http-equiv=\"X-UA-Compatible\" content=\"IE=8\" /><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />{0}</head><body>{1}</body></html>", AddStyles(), emailBody);

Then send the email:
SPUtility.SendEmail(web, false, false, recipient, subject, body)

This worked nicely, and the CSS styles were now applied consistently throughout the body of the email.