Wednesday, August 12, 2009

Unable to open Office documents from SharePoint

I started a job recently where the test environment had problems opening, creating or editing documents in document libraries. The documents in question were office documents, and the error we got when trying to create a new one was -

..."requires a windows sharepoint services-compatible application"..

Now I knew that there was nothing wrong with the client pc I was using to browse this site, as I could use the other environments without problem. So trying to track this problem down was a real issue because it is a common issue which manifests itself when office isn't configured correctly on the client... But I knew this wasn't the case.

So I tried to create another web application in this farm to see if the issue went away (which would of suggested configuration within the site)... But the new web app had the same issue. So this led me to believe it was either a central admin setting or an issue with a file in the 12 hive of the web server.

I quickly checked central admin for any settings which could create this issue, but I couldn't really see anything which would. So I concentrated on the 12 hive and had previously checked the C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\XML\DOCICON.XML file to see if the service account had enough permissions to read it (as it had been suggested elsewhere) but the service account did have enough permision. So I decided to move onto the rest of the 12 hive as I was convinced that everything else was in place and the only problems could come from the static 12 hive files.

So I used Beyond Compare to compare the 12 hive of the "broken" machine against a working instance of Sharepoint I had elsewhere.

When going through the differences I headed straight for the C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\XML\ folder as I had a feeling any issues were likely to be shown up in there... Sure enough when I compared the DOCICON.XML file there was one line that had been inserted by a previous developer -

The line that had been added to the non working xml file was -
<Mapping Key="pdf" Value="pdf.gif">

The tag wasn't closed correctly!!

So I changed it to -
<Mapping Key="pdf" Value="pdf.gif" />

And now my site was back up and working again.

You would think that this typo might have thrown a more obvious error than it did!

If this doesn't fix your issue the other 2 places I'd check (if it is definitely a server issue) are -
Permissions of DOCICON.XML
Document Template for the Content Type or Doc Library (make sure this isn't corrupt).


Hope this helps someone.

Thursday, May 28, 2009

Sharepoint - Activating features in code programatically

I have been working relentlessly on Workflow recently and although you can use the stsadm UpgradeSolution switch to deploy your new workflow sometimes the Association data needs changing which means all instances of the workflow need to be reinstalled.

Easy enough as i have a Workflow Association feature on each SubSite... but I didn't really want to go through each one and manually deactivate and reactivate.... and I also didn't want to use the stsadm command as it is far too slow. So I coded a quick console app to help with the deployment..



Console.WriteLine("Please enter the URL of the sharepoint site to install this against - e.g http://MySharepointSite.com");
string siteUrl = Console.ReadLine();
Console.WriteLine("Please paste the guid of the feature you would like to flush - e.g A7004706-0644-4aab-B097-691A5F51B3D6");
string strFeatureGuid = "" + Console.ReadLine();
Guid FeatureGuid;

try
{
site = new SPSite(siteUrl);
}
catch (Exception)
{

Console.WriteLine("ERROR: The site url does not relate to a Sharepoint site on this machine!! Press any key to close.");
Console.ReadKey();
return;
}

try
{
FeatureGuid = new Guid(strFeatureGuid);
}
catch (Exception)
{

Console.WriteLine("ERROR: The Guid entered is incorrect!! Press any key to close.");
Console.ReadKey();
return;
}

Console.WriteLine("Connected to site - " + site.Url.ToString() + ".");

int count = 0;
int flushCount = 0;
int sitesCount = site.AllWebs.Count;

foreach (SPWeb web in site.AllWebs)
{
foreach (SPFeature feat in web.Features)
{
if (feat.DefinitionId == FeatureGuid)
{
count++;
web.Features.Remove(FeatureGuid);
web.Features.Add(FeatureGuid);
flushCount++;
break;
}
}
web.Dispose();
}
Console.WriteLine("Feature is activated on " + count + " sites out of " + sitesCount + " sites.");
Console.WriteLine("Out of those " + count + " the Feature was successfully flushed on " + flushCount + " of them.");

count = 0;
foreach (SPWeb web in site.AllWebs)
{
foreach (SPFeature feat in web.Features)
{
if (feat.DefinitionId == FeatureGuid)
{
count++;
break;
}
}

}

Console.WriteLine("Feature is NOW activated on " + count + " sites out of " + sitesCount + ".");
Console.WriteLine("Please make a note of the above and Press any key to close");
Console.ReadKey();







Add this code to a console app and you can now use this to flush already activated features where needed.

Sunday, March 15, 2009

Sharepoint Solution Mangement - How will Redeploying my solution affect my Features?

Someone mentioned to me the other day that they were about to make updates to their Live Sharepoint site, this meant editing their Sharepoint Solution package (.wsp file) and redeploying, but they were nervous as they weren't sure what would happen.
The reason for their nerves was that they had a List Feature (with a SFeatureReceiver class) that created a list from a .stp file (with content in it). Since initial deployment their list had been edited and had new items in it, they didn't want the reployment to affect the content of this list.

So their options are -

stsadm -o upgradesolution command

or the following commands one after eachother

stsadm -o retractsolution
stsadm -o execadmsvcjobs
stsadm -o deletesolution
stsadm -o addsolution
stsadm -o deploysolution
stsadm -o execadmsvcjobs


Although there isn't much difference between the two options there are some subtle differences. The first command (stsadm -o upgradesolution) should be used to update what is already there in the solution. i.e Features that are already installed (There is a difference between Features in the 12 hive and Features that are fully installed).
The second set of commands can be used if you are adding a new Feature to your wsp. The new Feature will appear in the manifest.xml (as below).

Old Manifest.xml



New Manifest.xml

If you use the UpgradeSolution command for a new Feature then it will never get installed to your Site. This means you'll never see it from the Site Features or the Site Collection Features pages (Although it may well appear in the 12 hive).


The next question would be "Ok so I have a new Feature, but what happens to the already Installed and Activated Features if I run the above commands? Do they get ReActivated?"

The answer to this would be NO your already installed and Activated Features don't get reActivated... But they do get reinstalled! This means if you have a SPFeatureReceiver class like below then the only part which will be "run" is the Installed part -

This shouldn't affect anything in your site as all the work is done on the activation of the feature... Installing it really just means that the Feature is available throough the UI to activate.



Hope this cleared up a few Sharepoint grey areas.

Tuesday, February 17, 2009

Information management policy settings to Expire list items by it's Created Date

When I first used Sharepoints Information management policy settings I thought it would be easy to set an Expiration Policy on my list items. This seemed like a perfect fit for the requirement I was working on.

Was it Straight Forward? Yes and No.

But once I sorted out my issues I managed to quickly get it working. Firstly the reason I wanted to do this was because my client had a requirement to delete items in a list once they'd been there a couple of weeks. Easy I thought....

So I went ito the Settings Page of the List

Then I clicked on the Information management policy settings link.

I then selected Define a Policy.

On the following screen I checked the Enable Expiration box.

This gave me futher choices..... But the choice I wanted to select was greyed out! I wanted to select A time period based on the item's properties: ... but it wouldn't let me because I didn't have a date field to select from! This is when I started shouting "What about the Created Date" followed by "What about the Modified Date".

Once I'd calmed down I tried several ways to try and trick this policy by adding hidden fields to my list. It still didn't give me the desired result I was after.

So I needed a new plan....

So my attention turned to Site Collection Policies (Site > Site Settings > Site Collection Policies). This seemed like the way forward. Set a policy at the Site level and then select it from the List settings Information management policy settings page. But surely it will give me the same problem.... won't it?

When I clicked on the Site Collection Policies I got an Access Denied Error! So I started screaming - "I am a *#$%$#@ Site Collection administrator how can I not have access".

Well once I looked into it further I realised that I need some sort of permission to be able to access this page. So I decided to make sure I had all the available permissions assigned to my Web Application. I did this with the code below -

SPWebApplication WebApp = SPWebApplication.Lookup("http://MySitesUrl");

WebApp.RightsMask = WebApp.RightsMask | SPBasePermissions.FullMask;

WebApp.Update() ;


This can just as easily be done through the UI. See Gary Lapoints blog at - http://stsadm.blogspot.com/2007/10/enumerate-effective-base-permissions.html

Once I had done this I had access to the Site Collection Policies page.... Once there I added my expiration policy and then went and added it to my list through the Information management policy settings page.

After waiting for the Policy to run (This happens daily) I went back to double check my expire items had been deleted.... And they had. Success!

Hope this helped.

Wednesday, December 17, 2008

MOSS - Programatically ordering Navigation

From the title of this post most people would say - When would you ever need to do this?

Well I recently worked on a MOSS internet site which had a setup which split the Authoring and the Live site into two completely separate Farms. This provided quite a few challenges including coding a custom Import Export functionality.

The Import and Export functionality worked really well Except one overlooked issue - Navigation Ordering.

What if the Author reordered the navigation nodes in the Authoring Site how will they get reordered on the Live site? Well if the Site was a WSS site and it used the TopNavigationBar and the QuickLaunch Sharepoint Navigation objects then it would be pretty straight forward.... But Moss comes with its own Navigation/SiteMap Providers which are -
GlobalNavSiteMapProvider, CurrentNavSiteMapProvider and the CombinedNavSiteMapProvider.

The CombinedNavSiteMapProvider is (as the name suggests) a combination of the Global and CurrentNavSiteMapProviders. The GlobalNavSiteMapProvider is linked to the TopNavigationBar and any reordering of the GlobalNavSiteMapProvider will result in the order being reflected in the TopNavigationBar object (Web.Navigation.TopNavigationBar). The CurrentNavSiteMapProvider is linked to the QuickLaunch and any reordering here will result in the order being reflected in the QuickLaunch object(Web.Navigation.QuickLaunch).

Now here is where the whole process(for programmatically syncing navigation) gets a little tougher. If your Web hasn't been reordered then simply running the code below will fail because the nodes won't of been reflected in the Web.Navigation object yet.. So the SPNavigationNodeCollection.Count will be zero!

SPSite Site = new SPSite("http://mysite/");

SPWeb Web = Site.RootWeb;

SPNavigationNodeCollection SPNavNodes = Web.Navigation.QuickLaunch;



foreach (SPNavigationNode ThisSPNavNode in SPNavNodes)

{

ThisSPNavNode.MoveToFirst(SPNavNodes);

}

So how does the SPNavigationNodeCollection (QuickLaunch or TopNavigationBar) get populated? The answer is to go to the Site Navigation Setings page and change the order then click ok. And as if by magic the same code above will produce nodes which can be reordered (and this reordering will reflect in the Site Navigation Setings page).

So our Problem was that because the Items on the Live server hadn't been reordered(No Authoring is done on this server) then the SPNavigationNodeCollection (that we needed for us to reorder the navigation) wasn't populated.

The way we wanted the navigation sync to work was by producing an XML file (From the Authoring server like the one below) and we wanted to use it to reorder the Live servers Navigation nodes.

<Navigation>



<Web Name="" ServerRelativeURL="/">

<NavItem Title="SubSite 1" Url="/SubSite1" />

<NavItem Title="SubSite 2" Url="/SubSite2" />

<NavItem Title="SubSite 3" Url="/SubSite3" />

</Web>

<Web Name="SubSite1" ServerRelativeURL="/SubSite1">

<NavItem Title="Child 1 of SubSite 1" Url="/SubSite1/Child1ofSubSite1" />

<NavItem Title="Child 2 of SubSite 1" Url="/SubSite1/Child2ofSubSite1" />

<NavItem Title="Child 3 of SubSite 1" Url="/SubSite1/Child3ofSubSite1" />

</Web>

<Web Name="SubSite2" ServerRelativeURL="/SubSite2">

<NavItem Title="Child 1 of SubSite 2" Url="/SubSite2/Child1ofSubSite2" />

<NavItem Title="Child 2 of SubSite 2" Url="/SubSite2/Child2ofSubSite2" />

<NavItem Title="Child 3 of SubSite 2" Url="/SubSite2/Child3ofSubSite2" />

</Web>

<Web Name="SubSite3" ServerRelativeURL="/SubSite3">

<NavItem Title="Child 1 of SubSite 3" Url="/SubSite3/Child1ofSubSite3" />

<NavItem Title="Child 2 of SubSite 3" Url="/SubSite3/Child2ofSubSite3" />

<NavItem Title="Child 3 of SubSite 3" Url="/SubSite3/Child3ofSubSite3" />

</Web>

<Navigation>

So we took the XML content and looped through each web node and from the bottom we added each NavItem to (in this case) the QuickLaunch SPNavigationNodeCollection if it didn't already exist. We then moved that node to the top of the ordering and also Timestamped it so we could delete none timestamped nodes later on (Old nodes which no longer exist). It also seemed (for reasons beyond me) that you need to add this node with a NodeTypes of Page (even when the NodesType is really a web!). If you don't add the node as NodeTypes.Page the synching with the relevant "NavSiteMapProvider" fails.

The code below is how we went about it


XmlDocument xmlDoc = LoadXML("OurNavigation.xml");

XmlNode XmlNodes = xmlDoc.SelectSingleNode("//Navigation");



if (XmlNodes != null)

{

foreach (XmlNode ThisXmlNode in XmlNodes)

{

SPWeb NavWeb = Site.OpenWeb(ThisXmlNode.Attributes["ServerRelativeURL"].Value.ToString());

if (NavWeb != null)

{

PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(NavWeb);

if (pubWeb != null)

{

NavWeb.AllowUnsafeUpdates = true;



SPNavigationNodeCollection SPNavNodes = NavWeb.Navigation.QuickLaunch;



//Imported navigation Nodes

XmlNodeList WebsNavNodes = ThisXmlNode.ChildNodes;



for (int i = (WebsNavNodes.Count - 1); i != -1; i--)

{

bool found = false;

foreach (SPNavigationNode ThisSPNavNode in SPNavNodes)

{



string ThisPageRelativeUrl = ThisSPNavNode.Url;

string ThisPageRelativeTitle = ThisSPNavNode.Title;

if (WebsNavNodes[i].Attributes["Url"].Value == ThisPageRelativeUrl && WebsNavNodes[i].Attributes["Title"].Value == ThisPageRelativeTitle)

{

ThisSPNavNode.Properties["TimeStamp"] = TimeStamp;

ThisSPNavNode.Properties["NodeType"] = "Page";

ThisSPNavNode.Update();

ThisSPNavNode.MoveToFirst(SPNavNodes);

found = true;

break;

}

}

if (!found)

{

SPNavigationNode SPNode = SPNavigationSiteMapNode.CreateSPNavigationNode(WebsNavNodes[i].Attributes["Title"].Value, WebsNavNodes[i].Attributes["Url"].Value, NodeTypes.Page, NavWeb.Navigation.QuickLaunch);

SPNode.Properties["TimeStamp"] = TimeStamp;

SPNode.Update();

}

}

}

else

{

//Log.Write("Navigation Sync - The publishing web is null. The Navigation Web's URL is - " + NavWeb.Url + ". Please investigate.");

}

NavWeb.Dispose();

}

else

{

//Log.Write("Navigation Sync - Navigation Web could not be opened! The URL - " + ThisXmlNode.Attributes["ServerRelativeURL"].Value.ToString() + " May be incorrect or the Web might have had problems when it was imported. Please investigate.");

}

}





//****Delete Non timestamped Navigation Items****

foreach (SPWeb ThisWeb in Site.AllWebs)

{

SPNavigationNodeCollection NavNodes = ThisWeb.Navigation.QuickLaunch;

for (int i = (NavNodes.Count - 1); i != -1; i--)

{

SPNavigationNode ThisNode = NavNodes[i];

if (ThisNode != null)

{

string ThisNodeTimeStamp = "";

if (ThisNode.Properties["TimeStamp"] != null)

{

ThisNodeTimeStamp = ThisNode.Properties["TimeStamp"].ToString();

}

else

{

ThisNodeTimeStamp = "NoTimestamp";

}



if (ThisNodeTimeStamp != TimeStamp)

{

//Delete The Nav Node as it isn't in the Navigation XML file

ThisNode.Delete();

}

}

}



ThisWeb.Update();

ThisWeb.Dispose();

}

}

else

{

Log.Write("Navigation Sync - Problem with the XML Navigation Doc");

}

If you are interested in exporting your Navigation to an XML file similar to the one above then you can use the code below. Because we needed to use the "NavSiteMapProviders" to get the Navigation (We couldn't use the QuickLaunch or the TopNavigationBar as we couldn't be sure they were populated) then we had to use some sort of page so we were working within the SPContext. The way I got around this was to code a server control (code below) and then insert this in to a page which I then savedc to the _layouts directory (C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS).




protected override void OnLoad(EventArgs e)
{
try
{
SPSite Site = SPContext.Current.Site;
StringBuilder NavDoc = new StringBuilder();
NavDoc.Append("<Navigation>");
PortalSiteMapProvider _provider;

SiteMapProvider siteMapProvider = SiteMap.Providers["CurrentNavSiteMapProvider"];

_provider = siteMapProvider as PortalSiteMapProvider;
_provider.IncludePages = PortalSiteMapProvider.IncludeOption.Always;
_provider.IncludeSubSites = PortalSiteMapProvider.IncludeOption.Always;
_provider.IncludeHeadings = false;

SPWebCollection Webs = Site.AllWebs;

foreach (SPWeb Web in Webs)
{
NavDoc.Append("<Web Name=\"" + Web.Name + "\" ServerRelativeURL=\"" + Web.ServerRelativeUrl + "\">");
SiteMapNode siteNode = _provider.FindSiteMapNode(Web.ServerRelativeUrl);
SiteMapNodeCollection nodes = _provider.GetChildNodes(siteNode);

foreach (SiteMapNode node in nodes)
{
NavDoc.Append("<NavItem Title=\"" + node.Title + "\" Url=\"" + node.Key + "\" />");
}
NavDoc.Append("</Web>");
Web.Dispose();

}

NavDoc.Append("</Navigation>");

XmlDocument ThisXMLdoc = new XmlDocument();

ThisXMLdoc.InnerXml = NavDoc.ToString();

//Below I save the file with the Helper class
ImportExport.XmlHelper.SaveToFile(ThisXMLdoc, ImportExportFilePath + ImportExportFileName + ".xml");
netcreds = XmlHelper.GetFtpCredentials(RootWeb);
MoveFileToFtp();
}
catch(Exception ex)
{
//Log.Write("Error Exporting the Navigation Nodes. Exception - " + ex.Message.ToString());
}
}

To call the page I used the code below from a job in the TimerJobSchedule list(See previous post - http://my-sharepointer.blogspot.com/2008/12/sharepoint-timerjobscheduler-easier-way.html)


string NavUrl = SiteUrl + "/_layouts/mrwaNavigationExport.aspx";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(NavUrl);
request.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation;
request.UseDefaultCredentials = true;
request.PreAuthenticate = true;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

Hope it helps
Damian

Tuesday, December 16, 2008

Wiki Pages Added to Quick Launch Navigation

When creating a Wiki Site and having a look around I realised that new Wiki pages don't get added to the Quick Launch Navigation Menu. This seems a bit odd to me as the two default pages do get added. Hmmm

So I decided I'd quickly throw together a Sharepoint EventReceiver to do the trick. I override the ItemAdded (Needs to be done on the ItemAdded method as item isn't considered to be created yet so can't be added as a valid link from the ItemAdding method) method of the SPItemEventReceiver.

All you need to do is Add this EventReceiver to the Wiki Pages Document Library. This means everytime a Wiki Page gets Added to the Wiki Pages Document Library it will add the URL of that Wiki Page to the Quick Launch Menu (Under Wiki Pages).

Code Below


namespace MRWA.Wiki.FeatureReceivers
{
class AddPageToNavigationEventReceiver : SPItemEventReceiver
{

public override void ItemAdded(SPItemEventProperties properties)
{
SPListItem Item = properties.ListItem;

SPWeb Web = properties.ListItem.Web;



foreach (SPNavigationNode WikiPagesNode in Web.Navigation.QuickLaunch)
{
if (WikiPagesNode.Title == "Wiki Pages")
{
string ItemUrl = Web.ServerRelativeUrl + "/Wiki Pages/" + Item["FileLeafRef"].ToString();
string ItemName = Item["FileLeafRef"].ToString().Replace(".aspx", "");

bool CreateNavNode = true;
foreach (SPNavigationNode ChildNode in WikiPagesNode.Children)
{
if (ChildNode.Title == ItemName && ChildNode.Url == ItemUrl)
{
CreateNavNode = false;
break;
}

}

if (CreateNavNode)
{
SPNavigationNode NavNode = new SPNavigationNode(ItemName, ItemUrl);
WikiPagesNode.Children.AddAsLast(NavNode);

}

break;
}
}


base.ItemAdding(properties);
}
}
}

Sharepoint TimerJobScheduler - The easier way of scheduling SPTimerJobs

Ever since I've been working with Sharepoint I have coded Sharepoint Timer Jobs to run those tasks you want scheduled every day (or even every hour). Sharepoint is certainly a platform that lends itself to scheduling of batch process, with all the publishing of data there are going to be times when you want to schedule code to run at regular intervals.

So what are our options.... Well currently your only real option is to code a Sharepoint Timer Job solution which inherits SPJobDefinition and then code a SPFeatureReceiver which installs a Timer Job and points it at your newly created SPJobDefinition code.

This is all well and good but the real fun and games start when you want to change when the Job runs or how often it runs... What if you change you mind several times? (Well it has been known to happen). This is where SPTimerJobs really fail to live up to expectation.

So I decided to code a easily installable solution where Jobs can be Scheduled from a Sharepoint List which sits in the RootWeb of the Sharepoint Site. Now we can change the Days and Hours the Job, runs not to mention we can also start the job automatically. We can aslo Disable the job easily too.

Download the Sharepoint TimerJobScheduler - http://www.codeplex.com/SPTimerScheduler

Just download the zip file and follow the release notes to install the Feature into the site. Then you are ready to add your own code as Job Instances to the list.

This solution is a SPTimerJob which looks at the TimerJobSchedule List in the RootWeb and runs any TimerJobInstances when the Current Day and Hour is equal to the DaysToRun and HoursToRun fields.

The underlying SPTimerJob runs every 2 minutes which means when you go into the Instance and check the RunAtNextOpportunity checkbox, your TimerJobInstance will run almost immediately.



Once you've set the Days and Hours to run you need to set the Assembly, Class, Method and ConstructorParameters. The Assembly needs to be strong named and placed in the GAC. The Assmebly name will need to be fully Qualified (e.g SharePointerTimerJobScheduler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=8e20aa8796940e26).
The Class name will need to be the full path including the namespace (e.g SharePointer.Utilities.TestTimerSchedule).
The Method name doesn't need to include the brackets (e.g Execute).
If ConstructorParameter1 is null then the TimerJobScheduler assumes that this is a Class with a default constructor. (If ConstructorParameter2 is null then it assumes it is a Class with a constructor that has a single parameter.. And so on).



Code Example



using Microsoft.SharePoint;
namespace SharePointer.Utilities
{
public class TestTimerSchedule
{
SPSite Site;
SPWeb Web;
string ListName;
string ListItemPrefix;
public TestTimerSchedule(string strURL, string webURL, string strListName)
{
Site = new SPSite(strURL);
Web = Site.OpenWeb(webURL);
ListName = strListName;
ListItemPrefix = "New Item";
}
public void Execute()
{
if (!ListAlreadyExist(Web.Lists))
{
CreateTestList();
}
Web.AllowUnsafeUpdates = true;
SPListItem Item = Web.Lists[ListName].Items.Add();
Item["Title"] = ListItemPrefix + " added at - " + DateTime.Now.ToString();
Item.Update();
}
private void CreateTestList()
{
Web.Lists.Add(ListName, "A list to test the execution of the TimerJobScheduler", SPListTemplateType.GenericList);
}
private bool ListAlreadyExist(SPListCollection sPListCollection)
{
foreach (SPList list in sPListCollection)
{
if (list.Title == ListName)
{
return true;
}
}
return false;
}
}
}


Hope it helps
Cheers