Monday, March 22, 2010

Correcting SharePoint Lookup Columns

As we all know we can provision a custom lookup field using CAML by using the below code

<Field ID="{105620F4-9485-4e57-8184-902E604C3C78}"

Type="Lookup"
Name="CustomerName"
StaticName="Customer Name"
DisplayName="Customer Name"
Required="TRUE"
List="{64DFEC00-AD66-4F5F-AA1F-C2E78CE6680C}"
ShowField="Title"
Group="MTD Fax Log custom Fields"
PrependId="TRUE">
</Field>

If we observe the above code more closely, there is a property called "List" like this
List="{64DFEC00-AD66-4F5F-AA1F-C2E78CE6680C}"

The GUID which is present in the list property is the SharePoint Lookup list GUID. Many say that there is no need to give the GUID of the SharePoint lookup list, instead it can be used like this

List="Lists/Customers"

But that did not really work for me. Many say that before creating a sharePoint lookup field with a lookup list, that lookup list should actually exist. I have even tried this way, But I was unsucessful. As per me, I would say that SharePoint is not that intelligent to figureout if the lookup list actually exist then automatically set "List" property to that list, otherwise don't provision this lookup column and report error. It should be capable of doing this atleast in the future versions.

Now, how did I resolve this issue then? My problem is slightly different. As we all know that site columns always exists in the site collection level, then is it mandatory for the lookup list to exist at the site collection level? I want this sharepoint lookup column which is present at the site collection level to use a SharePoint lookup list which is present in one of the subsites in that site collection? Is it really possible? Yes it is possible.

Solution is that we

  1. Deploy SharePoint custom Lookup field and related content type which uses this site column, as one feature at the site collection level, with a dummy list GUID.
  2. Then we create another feature at the sub site level, in which there is a list definition which uses the site content type with the SharePoint custom Lookup site column in that, as another feature.

So when we activate the second feature then the pre-requisite is that the first feature at the site collection level should be already activated. So how do I check this before activating my sub site feature?

Check this post for more details.

If the parent site feature is successfully active, then while activating the sub site feature I will execute SharePoint object model code in the "Feature handler's Feature activated method" to correct the SharePoint custom lookup column List GUID like this.

class FeatureReceiver : SPFeatureReceiver

{
const string customersListURL = "/Lists/Customers";
const string customersLookupColumnGUID = "105620F4-9485-4e57-8184-902E604C3C78";
const string customersListTitle = "Customers";
const string customersListDescription = "List to hold Customer details";
const string customersListTemplateName = "Customers Profile Custom List";
const string parentFeatureID = "07FAC810-B8B7-4501-B905-372236479BA5";
Guid childFeatureID = new Guid("2f53989d-5799-4f65-bce8-0e1e7c1ae6c4");

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWeb web = SPContext.Current.Web;
web.AllowUnsafeUpdates = true;
SPSite site = web.Site;
SPWeb siteRootWeb = web.Site.RootWeb;
siteRootWeb.AllowUnsafeUpdates = true;
try
{
//Check for parent feature(Content Types and custom Fields) activeness
if (checkForParentFeatureActive(site, parentFeatureID))
{
// get a reference to the "Customers" list
SPList customersList = null;
if (checkIfListExist(web.Lists, customersListTitle))
{
if (web.ServerRelativeUrl == "/")
{
customersList = (SPList)web.GetList(customersListURL);
}
else
{
customersList = (SPList)web.GetList(web.ServerRelativeUrl + customersListURL);
}
}
// if the "Customers" list exists
if (customersList != null)
{
//correct customers Lookup Column
ChangeSiteColumnList(siteRootWeb, web, new Guid(customersLookupColumnGUID), customersList.ID);

}
else
{
//Create customer list if it does not exist
Guid guid;

guid = CreateListFromTemplate(web, customersListTitle, customersListTemplateName, customersListDescription);
customersList = web.Lists[guid];
customersList.OnQuickLaunch = true;
customersList.Update();
//correct customers Lookup Column
ChangeSiteColumnList(siteRootWeb, web, new Guid(customersLookupColumnGUID), guid);
}
}
else
{
/*deactivate the current feature and throw a error message to the user saying that
he has to activate the parent feature before activating this feature*/
web.Features.Remove(childFeatureID, true);
SPUtility.TransferToErrorPage("Please activate the parent feature(MTD Fax Log Content Types) on the site collection before activating this feature.");
}
}
catch (System.Threading.ThreadAbortException ex)
{
PortalLog.LogString(ex.StackTrace);
}
catch (System.Exception ex)
{
PortalLog.LogString(ex.StackTrace);
throw new SPException(ex.Message);
}
finally
{
web.Update();
web.AllowUnsafeUpdates = false;
siteRootWeb.Update();
siteRootWeb.AllowUnsafeUpdates = false;
siteRootWeb.Dispose();
site.Dispose();
}
}

private bool ChangeSiteColumnList(SPWeb rootWeb, SPWeb web, Guid siteColumnGuid, Guid lookupListGuid)
{
if (!(rootWeb.Fields[siteColumnGuid] is SPFieldLookup))
return false;
// cast lookup field
SPFieldLookup field = (SPFieldLookup)rootWeb.Fields[siteColumnGuid];
// change field lookup list id
field.SchemaXml = field.SchemaXml.Replace(field.LookupList.ToString(), lookupListGuid.ToString());
// reload field (a msdn article says you need to reload the column after changing the SchemaXml
field = (SPFieldLookup)rootWeb.Fields[siteColumnGuid];
//we need to change the "field.WebID" to the web which contains the lookup lists.
//Otherwise SharePoint tries to find the list on the current web which results in empty lists and
//"List not Found Errors" in the SharePoint log if you use this lookup columns in any subsite.
//if lookup list is in the subsite and site column is in the site collection then "web.ID" is the lookup list subsite id.
field.LookupWebId = web.ID;
field.Update();
return true;
}

private Guid CreateListFromTemplate(SPWeb web, string listTitle, string listTemplateName, string listDescription)
{
// read list template
SPListTemplate listTemplate = web.ListTemplates[listTemplateName];
// create list
return web.Lists.Add(listTitle, listDescription, listTemplate);
}

private bool checkIfListExist(SPListCollection ListCollection, string ListName)
{
bool flag = false;
foreach (SPList list in ListCollection)
{
if (list.Title.Equals(ListName))
{
flag = true;
break;
}
}
return flag;
}
//"Site" is the Root site collection
//"FeatureID" is the parent feature at the sitecollection level
private bool checkForParentFeatureActive(SPSite site, string featureID)
{
//This feature collection contains only features that are active.
SPFeatureCollection featureCollection = site.Features;
foreach (SPFeature feature in featureCollection)

{
if (feature.DefinitionId.ToString().ToLower() == featureID.ToLower())
{
return true;
}
}
return false;
}
Note: Most of the help for Correcting has been copied from this article.
http://msdn.microsoft.com/en-us/library/ms196289.aspx

Feature Activation Dependency – Activating multiple features using single feature

Feature activation dependency means activating multiple hidden features internally on activating a single visible feature. There are some rules to be followed for feature activation dependency

  1. There should be a single visible feature, and a set of hidden features. On activating this visible feature should activate all the hidden features.

  2. All the features should be scoped at the same level i.e either to "Web,
    Site, Web Application or Farm" level.

You might ask me when we will use this Feature activation dependency.

Let's consider a scenario. Let's assume we have created couple of SharePoint List Definitions and we want to please them in a single SharePoint solution and active them in a single feature as shown below.




As we can see from the above there are four "SharePoint Custom list definitions" and a single feature which provisions all these Custom List Definitions.


That single feature definition code actually looks like this…
<?xml version="1.0" encoding="utf-8"?>
<Feature Id="2f53989d-5799-4f65-bce8-0e1e7c1ae6c4"
Title="Fax Log"
Description="Description for Fax Log"
Version="12.0.0.0"
Hidden="FALSE"
Scope="Web"
DefaultResourceFile="core"
ImageUrl="GenericFeature.gif"
ReceiverAssembly="FaxLog, Version=1.0.0.0, Culture=neutral, PublicKeyToken=53432c81e5329871"
ReceiverClass="FaxLog.FeatureReceiver"
xmlns="http://schemas.microsoft.com/sharepoint/"&gt;
<ActivationDependencies>
<!--Currency Custom List -->
<ActivationDependency FeatureId="5E134998-AA1B-44bb-A553-6F9B75DC5AB1" />
<!--Customers Profile Custom List -->
<ActivationDependency FeatureId="01238751-CD20-4edf-9833-80C610850DA5" />
<!--Branch Total Custom List -->
<ActivationDependency FeatureId="C2306EDC-338B-4073-807B-6132512FA9CA" />
<!--Fax Log Custom List -->
<ActivationDependency FeatureId="B35CE3FE-B4FB-4a66-B817-5C418FAA0682" />
</ActivationDependencies>
</Feature>
Even Microsoft does same thing for creating all the lists, while provisioning a SharePoint site. See the feature code for team site provisioning.


<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="00BFEA71-4EA5-48D4-A4AD-7EA5C011ABE5" Title="$Resources:core,teamcollabFeatureTitle;" Description="$Resources:core,teamcollabFeatureDesc;" ImageUrl="WssTeamCollaborationFeature.gif" ImageUrlAltText="" Scope="Web" DefaultResourceFile="core" xmlns="http://schemas.microsoft.com/sharepoint/">
<ActivationDependencies>
<ActivationDependency FeatureId="00BFEA71-D1CE-42de-9C63-A44004CE0104" />
<!-- AnnouncementsList Feature -->
<ActivationDependency FeatureId="00BFEA71-7E6D-4186-9BA8-C047AC750105" />
<!-- ContactsList Feature -->
<ActivationDependency FeatureId="00BFEA71-DE22-43B2-A848-C05709900100" />
<!-- CustomList Feature -->
<ActivationDependency FeatureId="00BFEA71-F381-423D-B9D1-DA7A54C50110" />
<!-- DataSourceLibrary Feature -->
<ActivationDependency FeatureId="00BFEA71-6A49-43FA-B535-D15C05500108" />
<!-- DiscussionsList Feature -->
<ActivationDependency FeatureId="00BFEA71-E717-4E80-AA17-D0C71B360101" />
<!-- DocumentLibrary Feature -->
<ActivationDependency FeatureId="00BFEA71-EC85-4903-972D-EBE475780106" />
<!-- EventsList Feature -->
<ActivationDependency FeatureId="00BFEA71-513D-4CA0-96C2-6A47775C0119" />
<!-- GanttTasksList Feature -->
<ActivationDependency FeatureId="00BFEA71-3A1D-41D3-A0EE-651D11570120" />
<!-- GridList Feature -->
<ActivationDependency FeatureId="00BFEA71-5932-4F9C-AD71-1557E5751100" />
<!-- IssuesList Feature -->
<ActivationDependency FeatureId="00BFEA71-2062-426C-90BF-714C59600103" />
<!-- LinksList Feature -->
<ActivationDependency FeatureId="00BFEA71-F600-43F6-A895-40C0DE7B0117" />
<!-- NoCodeWorkflowLibrary Feature -->
<ActivationDependency FeatureId="00BFEA71-52D4-45B3-B544-B1C71B620109" />
<!-- PictureLibrary Feature -->
<ActivationDependency FeatureId="00BFEA71-EB8A-40B1-80C7-506BE7590102" />
<!-- SurveysList Feature -->
<ActivationDependency FeatureId="00BFEA71-A83E-497E-9BA0-7A5C597D0107" />
<!-- TasksList Feature -->
<ActivationDependency FeatureId="00BFEA71-C796-4402-9F2F-0EB9A6E71B18" />
<!-- WebPageLibrary Feature -->
<ActivationDependency FeatureId="00BFEA71-2D77-4A75-9FCA-76516689E21A" />
<!-- WorkflowProcessLibrary Feature -->
<ActivationDependency FeatureId="00BFEA71-4EA5-48D4-A4AD-305CF7030140" />
<!-- WorkflowHistoryList Feature -->
<ActivationDependency FeatureId="00BFEA71-1E1D-4562-B56A-F05371BB0115" />
<!-- XmlFormLibrary Feature -->
</ActivationDependencies>
</Feature>

Thursday, March 11, 2010

Check dependent (Parent) features for activation before activating child feature

One of the beautiful features that Microsoft provides is dependent (Parent) feature activation before activating its related sub features. For example if we try to activate "Office SharePoint Server Publishing Infrastructure" feature at the sub site level, before activating it at the parent site collection, it will redirect us to an application page under layouts "_layouts/ReqFeatures.aspx" saying that "One or more features must be turned on before this feature can be activated". This feature is very useful. Let me brief you how we can use this feature.

I was developing a highly customized SharePoint application for one of my clients where I got a situation like this. I was developing couple of "List definitions with Look up fields" which are to be deployed in one of the sub sites under a site collection. As we all know "List Definitions" only accept site columns and content types, I had to create Site Columns, Content Types that uses this site columns, for List definitions. Now the actual problem comes with the deployment. Problem is that "Site Columns and Content types" features should only be scoped at the "Site" i.e site collection level when we create them through feature, so I am forced to deploy them at the site collection level. I don't want List definitions to be deployed at the site collection level, since if we activate the feature on the site collection level, these list definitions are going to appear on all sub sites, which is not suggestible. So I thought I will deploy "Site Columns and Content types" scoped at site collection level and the "List definitions" scoped at the sub site level. What happens if the user activates sub site i.e "List Definitions" feature before activating required site collection i.e "Site columns and Content Types" feature

I was thinking of how to solve this problem then suddenly "Office SharePoint Server Publishing Infrastructure" stuff flashed my mind. How did Microsoft implement this? I thought I can control the sub site feature activation in the "Feature activated" feature receiver of the sub site, but by the time we get the control in the feature activated, feature receiver our sub site feature is already active. Then I was thinking why Microsoft didn't provide "Feature Activating" feature receiver. Finally here is the solution, to overcome this situation, in the feature activated feature receiver we will have to manually check if the required site collection features i.e "Site Columns and Content Types" is already activated, if it is activated then allow then allow this sub site feature to be activated successfully, otherwise deactivate this sub site feature and redirect the user to the error page with a message like "Please activate the parent feature(Site Columns and Content Type) on the site collection before activating this feature".

Now to check if the required feature is active on the site collection level I have written the following method.

//"Site" is the Root site collection

//"FeatureID" is the parent feature at the sitecollection level

private bool checkForParentFeatureActive(SPSite site, string featureID)

{
//This feature collection contains only features that are active.
SPFeatureCollection featureCollection = site.Features;

foreach (SPFeature feature in featureCollection)
{
if (feature.DefinitionId.ToString().ToLower() == featureID.ToLower())
{
return true;
}
}
return false;
}

We should keep in mind that below statement returns only features that are active on the site collection level.

SPFeatureCollection featureCollection = site.Features;

Below is the call to that method in the feature activated code

public override void FeatureActivated(SPFeatureReceiverProperties properties)

{

SPWeb web = SPContext.Current.Web;
web.AllowUnsafeUpdates = true;
SPSite site = web.Site;
try
{
//Check for parent feature(Content Types and custom Fields) activeness
if (checkForParentFeatureActive(site, parentFeatureID))
{
//do the necessary action if the parent feature is active.
}
else
{
/*deactivate the current feature and throw a error message to the user saying that he has to activate the parent feature before activating this feature*/

web.Features.Remove(childFeatureID, true);
SPUtility.TransferToErrorPage("Please activate the parent feature(Site Columns and Content Type) on the site collection before activating this feature.");
}
}
catch (System.Threading.ThreadAbortException ex)
{

PortalLog.LogString(ex.StackTrace);
}
catch (System.Exception ex)
{
PortalLog.LogString(ex.StackTrace);
throw new SPException(ex.Message);
}
finally
{
web.Update();
web.AllowUnsafeUpdates = false;
site.Dispose();
}

In the above code snippet, following code deactivates our sub site feature i.e the current feature

web.Features.Remove(childFeatureID, true);

and then redirect the user to the error page displaying a message.

Monday, March 8, 2010

Show attachments on SharePoint Discussion board Flat/Threaded views

As we all know, default OOB SharePoint Discussion Boards does not show attachments on Flat/Threaded views. If the user wants to view the attachments he had to click on "View Properties" and then view the attachments which is very inconvenient for the user. Moreover the attachment icon beside "View Properties" link is very small, hard to notice and is not linked to attachment. Due to this problem many of my business users were missing the attachments which are attached with the discussion. Due to this problem I had to find a way to show attachments within the Flat/Threaded views along with the content like this.


I started thinking for a way to implement it. Searched Google but couldn't find any solutions and finally one good idea flashed in my mind, i.e why shouldn't I try with jQuery I thought. Started looking into jQuery to implement it and finally here is the solution below.


<script type="text/javascript" src="../../Shared Documents/jquery-1.4.2.js"></script>
<script type="text/javascript">
//This script does not include attachments ICON's
$(document).ready(function(){
$('.ms-disc-padabove img[src*="attach.gif"]').each(function(index) {
var divAttachments;
var displayPageURL = $(this).parents().eq(2).next().find('a')[0].href;
displayPageURL += " #idAttachmentsTable";
if(index == 0)
{
divAttachments = "<br/><div id='divAttachments"+index+"'></div>";
$($($($(this).parents('.ms-disc-padabove')[0]).parent().next()[0]).find('div')[1]).append($(divAttachments));
}
else
{
divAttachments = "<br/><div id='divAttachments"+index+"'></div>";
$($($($(this).parents('.ms-disc-padabove')[0]).parent().next()[0]).find('div')[0]).append($(divAttachments));
}
$("#divAttachments"+index).load(displayPageURL);
});
});

//This script includes attachments ICON's
$(document).ready(function(){
//debugger;
//To display attachments
$('.ms-disc-padabove img[src*="attach.gif"]').each(function(index) {
var divAttachments;
var displayPageURL = $(this).parents().eq(2).next().find('a')[0].href;
if(index == 0)
{
divAttachments = "<br/><div id='divAttachments"+index+"'></div>";
$($($($(this).parents('.ms-disc-padabove')[0]).parent().next()[0]).find('div')[1]).append($(divAttachments));
}
else
{
divAttachments = "<br/><div id='divAttachments"+index+"'></div>";
$($($($(this).parents('.ms-disc-padabove')[0]).parent().next()[0]).find('div')[0]).append($(divAttachments));
}
//$($($($(this).parents('.ms-disc-padabove')[0]).parent().next()[0]).find('div > div')[0]).append($(divAttachments));
$.get(displayPageURL, function(data){
var attachmentsTable = $(data).find('#idAttachmentsTable');
$(attachmentsTable).find('a').each(function(index) {
var fileName = $(this)[0].innerHTML;
var fileExtension = fileName.substring(fileName.indexOf('.')+1).toLowerCase();
var imgTag;
if(fileExtension == 'xls' fileExtension == 'xlsx')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/icxls.gif'>&nbsp;"+fileName;
}
else if(fileExtension == 'doc' fileExtension == 'docx')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/icdoc.gif'>&nbsp;"+fileName;
}
else if(fileExtension == 'ppt' fileExtension == 'pptx')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ICPPT'>&nbsp;"+fileName;
}
else if(fileExtension == 'txt')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ictxt.gif'>"+fileName;
}
else if(fileExtension == 'bmp')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ICBMP.gif'>&nbsp;"+fileName;
}
else if(fileExtension == 'jpeg')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ICJPEG.gif'>&nbsp;"+fileName;
}
else if(fileExtension == 'jpg')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ICJPG.gif'>"+fileName;
}
else if(fileExtension == 'png')
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ICPNG.gif'>"+fileName;
}
else
{
imgTag = "<IMG BORDER=0 ALT='"+fileName +"'title='" +fileName +"'SRC='/_layouts/images/ICGEN.gif'>&nbsp;"+fileName;
}
$(this)[0].innerHTML = imgTag;
});
$("#divAttachments"+index).append(attachmentsTable[0].outerHTML);
});
})
});
</script>