Monday, October 5, 2009

How to add web parts to publishing page layouts and make them available to all the pages through code?

Scenario:
How to add web parts to publishing page layouts and make them available to all the pages through code?

Explanation:
So whats the big deal? What is so special? This is the question that strikes in one's mind after reading the title of this post.

Well.. when custom web parts are added to publishing pages that are created from publishing page layouts, until the page is checked out, the web parts wont be displayed. (SharePoint thinks that the web parts are unsafe until the page is checked out for the first time even after strong naming them and marking them as safecontrols, etc..)

There are various ways to do this. Other than the one I am going to describe below, I have seen a project in which the web parts have been specified in the site definition thus marking the controls are safe.

To fix this issue, we have created a custom control that resides on the page layout which takes care of carrying out operations on page such as adding, deleting, moving and setting properties on web parts.

Code:
In the code-behind of the page layout add the pagelayout control in the OnInit() as below:
/// <summary>
/// Registers page layout control that adds web parts to the page.
/// </summary>
/// <param>Virtual path of XML File that contains web part information for the page</param>
/// <returns></returns>
protected void AddWebPartsToPage(String webpartsXMLFileUrl)
{
try
{
PageLayoutControl plc = new PageLayoutControl(webpartsXMLFileUrl);
this.Page.Controls.Add(plc);
}
catch (Exception exception)
{
if (ExceptionPolicy.HandleException(exception, "Iti Exception"))
{
}
}
}

Sample Xml that goes in the webpartsXMLFileUrl
<WebParts LastModified="9/5/2008">
<WebPartAction>
<action>Delete</action>
<typeName>WebPart1.WebPart</typeName>
<zoneIndex>0</zoneIndex>
</WebPartAction>
<WebPartAction>
<action>Delete</action>
<typeName>WebPart2.WebPart</typeName>
<zoneIndex>1</zoneIndex>
</WebPartAction>
<WebPartAction>
<assemblyName>WebPart1, Version=[Version], Culture=neutral, PublicKeyToken=[Token]</assemblyName>
<className>WebPart1.WebPart</className>
<zoneID>RightTop</zoneID>
<zoneIndex>0</zoneIndex>
<typeName></typeName>
<action>Add</action>
<Properties>
<!-- Properties for the web part go here -->
<!-- Example -->
<Property Key="Title" Type="string" Value="WebPart1 Title"/>
<Property Key="ChromeState" Type="PartChromeState" Value="Normal"/>
</Properties>
</WebPartAction>
<WebPartAction>
<assemblyName>WebPart2, Version=[Version], Culture=neutral, PublicKeyToken=[Token]</assemblyName>
<className>WebPart2.WebPart</className>
<zoneID>RightTop</zoneID>
<zoneIndex>1</zoneIndex>
<typeName></typeName>
<action>Add</action>
<Properties>
<!-- Properties for the web part go here -->
<Property Key="Title" Type="string" Value="WebPart1 Title"/>
<Property Key="ChromeState" Type="PartChromeState" Value="Normal"/>
</Properties>
</WebPartAction>
</WebParts>

And finally the PageLayoutControl code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Diagnostics;
using Microsoft.SharePoint.Portal.WebControls;
using System.Collections;
using System.Reflection;
using System.ComponentModel;
using System.Data;
using System.IO;

using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using System.Runtime.Remoting.Contexts;

[XmlRoot("PageLayoutControl")]
public class PageLayoutControl : WebControl
{
//constants for property simple property setter value tag replacements
public const string PSV_DOMAIN = "[DOMAIN]";
public const string PSV_ACCOUNTNAME = "[ACCOUNTNAME]";
public const string PSV_MACHINE = "[MACHINE]";
public const string PSV_LOGIN = "[LOGIN]";

#region Properties
private String _fileLocation;
public String FileLocation
{
get
{
return _fileLocation;
}
set
{
_fileLocation = value;
}
}
#endregion

#region Constructor
public PageLayoutControl(String fileLocation)
{
this.FileLocation = fileLocation;
}
#endregion

#region Overriding Events

/// <summary>
/// Overriding the onLoad event of Custom Control.
/// This control is executed with Header place holder is executed in the Page Layout.
/// This code executes once on the page, It checks whether the code is executed
/// on the page, and if not it executes and adds the key to page for this code execution.
/// <param name="e" type="EventArgs">e</param>
/// <returns></returns>
/// </summary>

protected override void OnLoad(EventArgs e)
{
//do the base load functions
base.OnLoad(e);

//look to see if our web has had this stuff run yet
const string KEY_CHK = "PageLayoutControl";

//generic dictionary to store the list of webparts we're going to work 
//with and what we'll do with each; Steve likes generics!  :-)
Dictionary<string, WebPartAction> wpList = null;

//web part action class to store info about web parts that we're changing
WebPartAction wpa = null;

//webpart class that will be used in the action class
System.Web.UI.WebControls.WebParts.WebPart xWp = null;

//assembly we'll use to load web parts we are adding
Assembly wpAsm = null;

//serializer to convert the xml file into objects
XmlSerializer slz = null;

try
{
//get the current web; not using "using" because we don't want to kill
//the web context for other controls that need it

SPWeb curWeb = SPContext.Current.Web;

//turn on unsafe updates so we can make changes on a GET
curWeb.AllowUnsafeUpdates = true;


SPFile oPageLayout = curWeb.GetFile(SPContext.Current.File.Url);


//now that we have the file, read it into an xml document
XmlDocument xDoc = new XmlDocument();
string path = Context.Server.MapPath(_fileLocation);
xDoc.Load(path);

//compare the web part modified dates with the page
string pageWebPartActionsLastMod = string.Empty;
bool processWebPartActionsXml = true;
if (oPageLayout.Properties.ContainsKey(KEY_CHK))
if (xDoc.SelectSingleNode("WebParts") != null)
if (xDoc.SelectSingleNode("WebParts").Attributes["LastModified"] != null)
{
pageWebPartActionsLastMod = xDoc.SelectSingleNode("WebParts").Attributes["LastModified"].InnerText;

if (oPageLayout.Properties[KEY_CHK].ToString() == pageWebPartActionsLastMod)
processWebPartActionsXml = false;
}

//conditional block for processing webpart XML actions file
if (processWebPartActionsXml)
{
//check the page out
if (oPageLayout.CheckOutStatus == SPFile.SPCheckOutStatus.None)
SPSecurity.RunWithElevatedPrivileges(delegate { oPageLayout.CheckOut(); });

try
{

//create a hashtable to store our web parts
wpList = new Dictionary<string, WebPartAction>();

//create a new serializer
slz = new XmlSerializer(typeof(WebPartAction));

int webpartCount = 0; 
//enumerate through the child nodes
foreach (XmlNode xNode in xDoc.FirstChild.ChildNodes)
{
try
{
//try serializing each one
wpa = slz.Deserialize(new System.IO.StringReader(xNode.OuterXml)) as WebPartAction;
}
catch (Exception exception)
{
//set our reference to null so we don't try and do something with
//a version that was successfully serialized previously
wpa = null;
}


//make sure we serialized, and also make sure this is an Add
if (wpa != null)
{
switch (wpa.action)
{
case WebPartAction.ActionType.Add:

//now try loading the assembly
try
{
wpAsm = Assembly.Load(wpa.assemblyName);
}
catch (Exception exception)
{
//set our reference to null so we don't try and do something with
//a version that was successfully loaded previously
wpAsm = null;
}

if (wpAsm != null)
{
//try creating an instance of the class
try
{
xWp = (System.Web.UI.WebControls.WebParts.WebPart)wpAsm.CreateInstance(wpa.className);
}
catch (Exception exception)
{
//set our reference to null so we don't try and do something with
//a version that was successfully loaded previously
xWp = null;
}

//plug it into our class
if (xWp != null)
{
//enumerate and add properties to part
SetWebPartProperties(wpa, xWp);

//add part to class
wpa.wp = xWp;
}
}

//add it to the hashtable
wpList.Add(Guid.NewGuid().ToString(), wpa);
break;
default:
//for delete, move or SetProperties we want to capture 
//the type name and plug it into the hash array
webpartCount++;
if ( !wpList.ContainsKey(wpa.typeName.ToString() + webpartCount.ToString()))
wpList.Add(wpa.typeName.ToString() + webpartCount.ToString(), wpa);
break;
}
}
}

//get the web part manager
SPLimitedWebPartManager theMan = oPageLayout.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared);
webpartCount = 0;
foreach (System.Web.UI.WebControls.WebParts.WebPart wp in theMan.WebParts)
{
//Increments webpart counter........
webpartCount++;
//check each web part to see if matches our typeName
if (wpList.ContainsKey(wp.GetType().ToString() + webpartCount.ToString()))
wpList[wp.GetType().ToString() + webpartCount.ToString()].wp = wp;
}

//now enumerate items in hash; can't do it in WebPart collection 
//on SPLimitedWebPartManager or it fails
foreach (string key in wpList.Keys)
{
wpa = wpList[key];
if (wpa.wp != null)
{
switch (wpa.action)
{
case WebPartAction.ActionType.Delete:
theMan.DeleteWebPart(wpa.wp);
break;
case WebPartAction.ActionType.Move:
theMan.MoveWebPart(wpa.wp, wpa.zoneID, int.Parse(wpa.zoneIndex));
theMan.SaveChanges(wpa.wp);
break;
case WebPartAction.ActionType.Add:
theMan.AddWebPart(wpa.wp, wpa.zoneID, int.Parse(wpa.zoneIndex));
break;
case WebPartAction.ActionType.SetProperties:
SetWebPartProperties(wpa, wpa.wp);
theMan.SaveChanges(wpa.wp);
break;
}
}

}

//add our key to the property bag so we don't run our provisioning code again
if (oPageLayout.Properties.Contains(KEY_CHK))
{
oPageLayout.Properties[KEY_CHK] = pageWebPartActionsLastMod;
}
else
{
oPageLayout.Properties.Add(KEY_CHK, pageWebPartActionsLastMod);
}


oPageLayout.Update();

// for making the page appear in edit mode when first provisioned.

////check the page in and publish
// to be executed when migrated pages which should have been published before.
if (oPageLayout.MajorVersion >= 1 )
{
SPSecurity.RunWithElevatedPrivileges(delegate
{
try
{
oPageLayout.CheckIn(string.Format(" -- checked in on {0}", DateTime.Now));
oPageLayout.Publish(string.Format(" -- published on {0}", DateTime.Now));
}
catch (Exception ex)
{ }
});
}
// To be executed when new site is create with a layout as default page.
if (oPageLayout.Versions.Count == 1 && oPageLayout.MinorVersion == 1 && oPageLayout.CheckOutStatus == SPFile.SPCheckOutStatus.None )
{
SPSecurity.RunWithElevatedPrivileges(delegate
{
try
{
oPageLayout.CheckOut(); 
}
catch (Exception ex)
{ }
});
}
curWeb.AllowUnsafeUpdates = false;


Context.Response.Clear();
Context.Response.Redirect(oPageLayout.Name + "?ControlMode=Edit&DisplayMode=Design", false);

//potentially delete our xml manifest file that tells us what to do
//would be cool if we could just remove ourselves from the page
//or maybe we stay there so we could be activated again in the future?? That
//would be an interesting idea...
}

catch (Exception exception)
{
if (ExceptionPolicy.HandleException(exception, "Iti Exception"))
{

}
}
}

}
catch (Exception exception)
{
}
}
#endregion

#region Custom Methods

/// <summary>
/// This method sets the WebPart Properties before placing them on the page.
/// It checks if the Web Part Properties are specified in the Xml file.
/// Property Name "Title" and "Associated List Name".
/// <param name="wpa" type="WebPartAction">wpa</param>
/// <param name="xWp" type="System.Web.UI.WebControls.WebParts.WebPart">xWp</param>
/// <returns></returns>
/// </summary>

private void SetWebPartProperties(WebPartAction wpa, System.Web.UI.WebControls.WebParts.WebPart xWp)
{
//check to see if there are any properties; if there are zero it won't say
//zero, it will say null (unlike vb.net)
if (wpa.Properties == null)
return;

//enumerate and add properties to part
for (int p = 0; p < wpa.Properties.Property.Length; p++)
{
try
{
// checks the "Type" attribute of property and calls the appropriate method to get property value.
switch (wpa.Properties.Property[p].Type.ToString().ToLower())
{
case "string":
xWp.GetType().GetProperty(wpa.Properties.Property[p].Key).SetValue(xWp,
GetPropertySetterValue(wpa.Properties.Property[p].Value), null);
break;
case "partchromestate":
xWp.GetType().GetProperty(wpa.Properties.Property[p].Key).SetValue(xWp,
GetPropertySetterPartChrome(wpa.Properties.Property[p].Value), null);
break;
}

}

catch (Exception exception)
{
if (ExceptionPolicy.HandleException(exception, "Iti Exception"))
{
//  throw;
}
}

}
}

/// <summary>
/// This is a property getter method which gets the WebPart Property
/// of Type string. This methods set the property for all string type properties.
/// Property Name "Title" and "Associated List Name".
/// <param name="Value" type="string">Value</param>
/// <returns name="ret" type="string">ret</returns>
/// </summary>

private string GetPropertySetterValue(string Value)
{
try
{
string ret = Value;

switch (Value)
{
case PSV_ACCOUNTNAME:
ret = Environment.UserName;
break;
case PSV_DOMAIN:
ret = Environment.UserDomainName;
break;
case PSV_MACHINE:
ret = Environment.MachineName;
break;
case PSV_LOGIN:
ret = Environment.UserDomainName + "\\" + Environment.UserName;
break;
default:
break;
}

return ret;
}
catch (Exception exception)
{
if (ExceptionPolicy.HandleException(exception, "Iti Exception"))
{
//  throw;
}
return null;
}

}

/// <summary>
/// This is a property getter method which gets the WebPart Property
/// of Type PartChromeState. Its has two states Normal / Minimized
/// <param name="Value" type="string">Value</param>
/// <returns name="chState" type="PartChromeState">chState</returns>
/// </summary>

private PartChromeState GetPropertySetterPartChrome(string Value)
{
try
{
PartChromeState chState = PartChromeState.Normal;

// checks if its find the xml attribute "Minimized"
// Sets the WebPart State to Minimized.
if (Value == "Minimized")
{
chState = PartChromeState.Minimized;
}
return chState;
}
catch (Exception exception)
{
return PartChromeState.Normal;
}
}

#endregion
}

#region WebPart Property Class

/// <summary>
/// Class defines the WebPart Actions the are required to place 
/// the WebParts by default when the instance of Page Layout is created.
/// These actions corresponds to xml elements that are nested under <PageLayout> </PageLayout> element.
/// </summary>

public class WebPartAction
{
/// <summary>
/// Enumerating the Actions that can be applied to WebParts.
/// </summary>
public enum ActionType
{
Delete,
Add,
Move,
SetProperties
}

#region WebPartAction Properties

[XmlIgnore()]
public System.Web.UI.WebControls.WebParts.WebPart wp = null;

/// <summary>
/// Specify Assembly Name.
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public string assemblyName = string.Empty;

/// <summary>
/// Specify Class Name.
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public string className = string.Empty;

/// <summary>
/// Specify Web Part Zone ID.
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public string zoneID = string.Empty;

/// <summary>
/// Specify Web Part Zone Index.
/// using a string to greatly simplify potential errors during deserialization.
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public string zoneIndex = "0";

/// <summary>
/// Specify Type Name.
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public string typeName = string.Empty;

/// <summary>
/// Specify Web Part Action (Add, Delete, Move or Set Properties).
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public ActionType action;

/// <summary>
/// Specify Web Part Properties.
/// </summary>
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public PropertyRoot Properties;

#endregion

#region Constructors

/// <summary>
/// Parameterless constructor for WebPart Action Class. 
/// <param></param>
/// <returns></returns>
/// </summary>
public WebPartAction()
{
//parameter-less constructor needed for serialization
}

/// <summary>
/// Parameterized constructor for WebPart Action Class. 
/// Used to deserilaize the xml with WebPart Actions defined. 
/// <param name="wp" type="System.Web.UI.WebControls.WebParts.WebPart">wp</param>
/// <param name="action" type="ActionType">action</param>
/// <returns></returns>
/// </summary>
public WebPartAction(System.Web.UI.WebControls.WebParts.WebPart wp, ActionType action)
{
this.wp = wp;
this.action = action;
}

/// <summary>
/// Parameterized constructor for WebPart Action Class. 
/// Used to deserilaize the xml with WebPart Actions, Zone ID and Zone Index defined. 
/// <param name="wp" type="System.Web.UI.WebControls.WebParts.WebPart">wp</param>
/// <param name="action" type="ActionType">action</param>
/// <param name="zoneID" type="string">zoneID</param>
/// <param name="zoneIndex" type="string">zoneIndex</param>
/// <returns></returns>
/// </summary>
public WebPartAction(System.Web.UI.WebControls.WebParts.WebPart wp, ActionType action, string zoneID, string zoneIndex)
{
this.wp = wp;
this.action = action;
this.zoneID = zoneID;
this.zoneIndex = zoneIndex;
}

#endregion


#region WebPart Property Class

/// <summary>
/// Class specify the webpart properties.
/// Get the array of properties in the form of xml elements,
/// with its attributes as property values.
/// </summary>

public class PropertyRoot
{
[XmlElement(Form = XmlSchemaForm.Unqualified)]
public OneProperty[] Property;
}

#endregion

#region Property Attributes Class

/// <summary>
/// Class defines the webpart property attributes.
/// Three attributes of property can be defined,
/// <Key> <Type> <Value>
/// </summary>
public class OneProperty
{
[XmlAttribute(Form = XmlSchemaForm.Unqualified)]
public string Key = string.Empty;

[XmlAttribute(Form = XmlSchemaForm.Unqualified)]
public string Type = string.Empty;

[XmlAttribute(Form = XmlSchemaForm.Unqualified)]
public string Value = string.Empty;
}
#endregion
}
#endregion

1 comment:

  1. do u really need that many namespaces


    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using Microsoft.SharePoint;
    using Microsoft.SharePoint.WebPartPages;
    using System.Xml;
    using System.Xml.Serialization;
    using System.Xml.Schema;
    using System.Diagnostics;
    using Microsoft.SharePoint.Portal.WebControls;
    using System.Collections;
    using System.Reflection;
    using System.ComponentModel;
    using System.Data;
    using System.IO;

    using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
    using Microsoft.Practices.EnterpriseLibrary.Logging;
    using System.Runtime.Remoting.Contexts;

    ReplyDelete