maandag 17 december 2007

Custom Timer Job

Recently I had to write a Custom Timer Job in SharePoint.
The idea was to remove old items from a contacts list.

All the items that were older then 5 days had to be removed from the list.
At first I intended to do this with the information management policy build in SharePoint lists. If you want to know more about how to do this follow this link.

After evaluating this option with my superior we agreed that it would be better if we would do this programmatically.
So I was asked to create a custom timer job programmatically.

I expected this to be a long quest for information but it turned out to be a very short quest to the blog of Andrew Connell.
Andrew already made a post on this matter. It helped me create my timer job.
I also found the code to be easy to understand.

The bottom line is that I'm creating a class that inherrits from the SPJobDefinition class already build in SharePoint. You have to add some constructors and override the Execute method to make it work as intended. Wrap this class in a feature and activate it at Site level. Voila, a working custom timer job.

For more information (and code examples) on how to make a custom timer job I recommend you to read Andrew Connell's post titled Creating Custom SharePoint Timer Jobs

vrijdag 16 november 2007

Form Based Authentication in SharePoint

I was asked recently to setup a SharePoint site with Form Based Authentication. My first reaction was...can't be that hard no...
After viewing a couple of tutorials and blogs I realized how wrong I was.
But for every problem there is a solution...

So I found a very good tutorial written by Andrew Connell(who else...)
So for all of you who don't know how to do this I glady direct you to Andrew's post on how to setup Form Based Authentication.

maandag 29 oktober 2007

Generate docx files

Recently I was asked to make a feature who would open a .docx file filled with the Active Directroy data from the currently logged user. I had to build a feature who would redirect me to an .aspx page (in fact an .ashx page but it'll become more clear by then end this post), from that page I had to retrieve the information of the
currently logged in user, get his data from Acitve Directory with an LDAP query and create a .docx file with Open XML which I have to fill with the data retrieved from Active Directory.
Right....let's start with the easy part, create a feature which redirects me to my .ashx page.
For our feature we need two files, a feature.xml and a elements.xml.
The feature.xml is a standard feature which points to our elemnts.xml where we will build our redirection CustomAction.
The code for the feature.xml file should look like this:

<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="724BB38A-051C-4ce5-844B-2F91FC51B66B"
Title="Create document from template"
Description="This creates a document from a template in this libarary"
Version="1.0.0.0"
Scope="Web"
Hidden="FALSE"
xmlns="http://schemas.microsoft.com/sharepoint">
<ElementManifests>
<ElementManifest Location="elements.xml" />
</ElementManifests>
</Feature>

The elements.xml file like this:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction Id="CreateDocFromTemplate"
RegistrationType="List"
GroupId="ActionsMenu"
Location="Microsoft.SharePoint.StandardMenu"
Title="Create document from template"
Description="This creates a document from a template in this libarary" >
<UrlAction Url="~site/_layouts/docgeneration/generatedoc.ashx" />
</CustomAction>
</Elements>

We see that in the elements.xml file we define a CustomAction which defines the Location - Title - Description. Like you see it here we are placing our link in the Actions menu dropdownlist for any document library.
The most important part is the UrlAction, this indicates to which page we want to go once we click on the link.
Now that we have our feature we can start with the real work. What we will do is create a .docx file filled with Active Directory data from the user currently logged on our SharePoint site. But the important thing to know here is, that we are not going to create a file which is stored on a physical drive but we will create the file inside a MemoryStream and return that stream in the Httphandler of our .ashx file. What we will obtain is a dialog box which appear once
we click the feature asking us if we want to save or open the file. Once we click Open the file will then be opened in a Word editor.

Create a new website in VS2005 and select the Empty Web Site template. We are adding a new Web Form to our web site, we will name it gendocx.ashx and delete the whole content of the file. Also delete the gendocx.ashx.cs file created with the gendocx.ashx file as we will put all our code in the first .ashx file.
The .ashx file consists of three code-part and the surrounding class declaration.
To be able to send our docx file to the client we need to implement the IHttphandler interface and add a @WebHandler.
To make it easy we’ll also add an @Assembly directive so we can program against the
Windows SharePoint Services object model.

<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ WebHandler Language="C#" Class="gendocx" %>


using System;
using System.IO;
using System.Xml;
using System.Web;
using System.Data;
using System.IO.Packaging;
using Microsoft.SharePoint;

public class gendocx : IHttpHandler {
public bool IsReusable {
get { return false; }
}

public void ProcessRequest(HttpContext context) {
}
}

Add a reference to the WindowsBase.dll and the Microsoft Office SharePoint Server Components assembly.
The first of code we will add now contains the basics command that will be processed when we activate the link.
Some function or code may seem unclear but they'll be explained later on.
Paste the following code inside the ProcessRequest method.

MemoryStream stream = new MemoryStream();
Package pack = Package.Open(stream, FileMode.Create, FileAccess.ReadWrite);
this.BuildDoc(pack);
pack.Close();
context.Response.ClearHeaders();
context.Response.AddHeader("content-disposition", "attachment; filename="+this.getNameFromAD()+".docx");
context.Response.ClearContent();
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
context.Response.ContentType = "application/vnd.ms-word.document.12";
stream.Position = 0;
BinaryWriter writer = new BinaryWriter(context.Response.OutputStream);
BinaryReader reader = new BinaryReader(stream);
writer.Write(reader.ReadBytes((int)stream.Length));
reader.Close();
writer.Close();
stream.Close();
context.Response.Flush();
context.Response.Close();

What is happening here? Well basicly we are creating a MemoryStream that we are filling with a Package, the Package on his turn is filled with all the xml data needed to create a docx file.
Once those two have been made we clear the header of our response which we are filling with our attachement file (read docx file). Every Open statment should be ended by a Close statement. We are also declaring writer and a reader who basicly help us to read the MemoryStream and write in our response header.
You have noticed that I have added two undeclared function beeing BuildDoc and getNameFromAD.

Let me first explain what the BuildDoc method does. We have to create our docx file. How do we do this? We create it build piece after piece with Open XML. If you are not familiar with Open XML I advice you to read this article (Building Server-Side document generation using the Open XML Object Model (MSDN - Erika Ehrli )) or the e-book by Wouter van Vugt (Open XML:The Markup Explained). A docx file is made of several smaller xml files which
combined together make a complete docx file. Take a look at the next chunk of code and you will understand what I'm talking about:

public void BuildDoc(Package pack)
{
Uri uri = new Uri("/word/document.xml", UriKind.Relative);
string partContentType;
partContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
PackagePart part = pack.CreatePart(uri, partContentType);
StreamWriter streamPart = new StreamWriter(part.GetStream(FileMode.Create, FileAccess.Write));
string nameSpace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
XmlDocument xmlPart = new XmlDocument();
XmlElement document;
document = xmlPart.CreateElement("w:document", nameSpace);
xmlPart.AppendChild(document);
XmlElement body;
body = xmlPart.CreateElement("w:body", nameSpace);
document.AppendChild(body);
XmlElement paragraph;
paragraph = xmlPart.CreateElement("w:p", nameSpace);
body.AppendChild(paragraph);
XmlElement row;
row = xmlPart.CreateElement("w:r", nameSpace);
paragraph.AppendChild(row);
XmlElement text;
text = xmlPart.CreateElement("w:t", nameSpace);
row.AppendChild(text);
XmlNode nodeText;
nodeText = xmlPart.CreateNode(XmlNodeType.Text, "w:t", nameSpace);
nodeText.Value = this.getNameFromAD();
text.AppendChild(nodeText);
xmlPart.Save(streamPart);
streamPart.Close();
pack.Flush();
string relation;
relation = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
pack.CreateRelationship(uri, TargetMode.Internal, relation, "relation");
pack.Flush();
}

The last part is getting the data from Active Directory. For this example we will only retrieve the name of the user currently logged in. We compare the mail of the user with the mail stored in Active Directory with an LDAP query.

public string getNameFromAD()
{
string userMail = SPContext.Current.Web.CurrentUser.Email.ToString();
DirectorySearcher objsearch = new DirectorySearcher();
string strrootdse = objsearch.SearchRoot.Path;
DirectoryEntry objdirentry = new DirectoryEntry(strrootdse);
objsearch.Filter = "((mail=" + userMail + "))";
objsearch.SearchScope = System.DirectoryServices.SearchScope.Subtree;
objsearch.PropertiesToLoad.Add("cn");
objsearch.PropertyNamesOnly = false;
objsearch.Sort.Direction = System.DirectoryServices.SortDirection.Ascending;
objsearch.Sort.PropertyName = "cn";
objsearch.PageSize = 1;
SearchResult result = objsearch.FindOne();
objsearch.Dispose();
return result.GetDirectoryEntry().Properties["cn"].Value.ToString();
}

Save your file. Go to your site and click on your feature, a dialog box should pop up and ask you if you want to open or save your file. Click open and there it is, a docx file filled with Active Directory data from our currently logged user.

Links I used:
Retrieving data from Active Directory with System.DirectoryServices – the right way
Generating Office 2007 documents in C#
Server-Side Generation of Word 2007 Docs"

maandag 22 oktober 2007

NavigationWebPart

Today I received the assignment to update a WebPart to SharePoint 2007 format.The WebPart we are talking about is the NavigationWebPart made by Jan Tielens. You can find the orignial post here

My first thought was to rewrite the whole code to make a completely new WebPart but after a while
I realized that with some modifications the code would work just as good.
So I started by removing some code and commments I didn't realy need for my assignment out of the orignial code.
Afterwards I changed the functions to make it work with SharePoint 2007

The final result looks like this.

using System.Web.UI;
using System.ComponentModel;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace NavigationWebPart
{
public class NavigationWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
private int _levels = 5;
private bool _startFromRoot = true;


[WebBrowsable(),
Personalizable(),
System.ComponentModel.Category("Navigation"),
WebDescription("How many levels do you want to allow."),
WebDisplayName("Levels"),
DefaultValue(5)]
public int Levels
{
get { return _levels; }
set { _levels = value; }
}

[WebBrowsable(),
Personalizable(),
System.ComponentModel.Category("Navigation"),
WebDescription("Start from root."),
DefaultValue(true)]
public bool StartFromRoot
{
get { return _startFromRoot; }
set { _startFromRoot = value; }
}

protected override void RenderContents(HtmlTextWriter writer)
{
writer.Write("<table border=0 width=100%>");
SPWeb web = SPControl.GetContextWeb(Context);
SPWebCollection subWebs = null;
writer.Write("<tr><td>");
if (StartFromRoot)
{
SPWeb rootWeb = SPControl.GetContextSite(Context).RootWeb;
subWebs = rootWeb.GetSubwebsForCurrentUser();
writer.Write("<a href='{0}'><b>{1}</b></a>", rootWeb.Url, rootWeb.Title);
}
else
{
writer.Write("<a href='{0}'><b>{1}</b></a>", web.Url, web.Title);
subWebs = web.GetSubwebsForCurrentUser();
}
writer.Write("</td></tr>");
writer.Write("<tr><td>");
writer.Write("<ul>");
foreach (SPWeb childSite in subWebs)
{
if (web.ID != childSite.ID)
{
writer.Write("<li><a href='{0}'>{1}</a></li>", childSite.Url, childSite.Title);
RenderWeb(writer, childSite, 1);
}
}
writer.Write("</ul>");
writer.Write("</td></tr>");
writer.Write("</table>");
}

private void RenderWeb(HtmlTextWriter writer, SPWeb web, int currentLevel)
{
SPWebCollection subWebs = web.GetSubwebsForCurrentUser();
writer.Write("<ul>");
foreach (SPWeb childSite in subWebs)
{
if (web.ID != childSite.ID)
{
if (currentLevel < this.Levels)
{
writer.Write("<li><a href='{0}'>{1}</a></li>", childSite.Url, childSite.Title);
int newLevel = currentLevel + 1;
RenderWeb(writer, childSite, newLevel);
}
}
}
writer.Write("</ul>");
}
}
}

Build and deploy this code like you would do with any other WebPart and you have a working NavigationWebPart again.
Thanks to some great code already existing and to some modifications.

WebParts: what you need to know!

Recently I started with a course about WSS 3.0 and like all courses they start with the classic 'Hello World' exercise. I'm not going to talk about how to make a 'Hello World' because there are plenty of posts where this is already described.I'll tell you where I had some troubles so that you can avoid them.
The creation of the 'Hello World' itself isn't realy a problem as we all know it it's just a print to your screen to learn the basic syntax of the appliction. But when you start building and deploying you may face some problems like I faced some myself.
Let's start with the build and deploy procedure.To build and deploy a WebPart you have two options. The firstone is to build and deploy your .dll to the GAC (Global Assembly Cache). The problem you will face here is that you have to strongly name you assembly for it to work. So before building your assembly make sure that you go to you project properties -> signing -> check the 'signing the assembly'checkbox, choose to make a new key file. You'll see that the newly made key file will be added to your project. Copy paste you assembly to the GAC ( C:\WINDOWS\assmebly ).The other possiblity is to copy/paste your .dll to the bin directory of the server you are working with.So go to c:\inetpub\wwwroot\wss\virtualdirectories\80\bin\ (for the default :80 port) and copy paste your .dll
Putting your .dll in the bin folder is not enough. You have to tell SharePoint that it's safe to import your WebPart.To do this we have to make some minor modifications to our web.config file. You'll find that file in the c:\inetpub\wwwroot\wss\virtualdirectories\80\ folder.Open the web.config and look for the <safecontrols>tags, at the bottom of the list you can add your <safecontrol>tag wich will tell SharePoint your WebPart is safe.To be sure you got the right value for your WebPart you can use Lutz Roeder's .NET Reflector.Simply open your .ddl in it and copy paste the needed value.Your SafeControl tag should look something like this.

<SafeControl Assembly="HelloWorldWP, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" Namespace="HelloWorldWP" TypeName="*" Safe="True" />

Note that if you have deployed your WebPart to the GAC and thus strongly named it you'll get a unique ID for the PublicKeyToken and not the value 'null' like in our example here.
Another problem I faced, not with the 'Hello World' application but with bigger WebPart is the lack of error messages SharePoint is providing. To counter this you can change your web.config file a bit so it'll tells you wich error has occured.To do so change the following things in your web.config:

<sharepoint><safemode callstack="true">
<system.web><customerrors mode="off">
<compilation debug="true" batch="true">

Now that you know what the error is you can start debugging. attach the w3wp.exe to your process and start debugging with VS2005.

I hope this will be helpfull as these were the 2 problems I faced when starting to develop WebParts.

woensdag 17 oktober 2007

First day!!!

Today was the day, for the next three months I'll be working as an intern for SharePoint at Dolmen I must say I was eager to start with this new challenge.
As a student I never worked with SharePoint so it's something completely new for me.
Not knowing what will happen scares me a bit but I know that with some help I will get through it.

Enough chatting let's talk a bit about my first day.
Like everybody on the first day of their new job I was very nervous,
on the way to Dolmen questions kept swinging around in my head.
Where am I going to work? What will I have to do? How are the people who will have to work with me? And many more questions.
So you can imagine I was really nervous when my promoter came down and led me to my desk. But after all I really had no reason
to feel nervous. The people I met there were friendly and helpful. We got introduced to each other and after some
heavy working with some computers I got my spot in the office.
After the usual computer configuration I received an e-mail asking me to attend at a presentation about WSS 3.0.
Right down to business I thought to myself.
I felt like a new world was opening before me at the presentation, yes off course there were moments where I got lost
but after all I understood the big picture and that was the most important thing I think.
After the presentation I received a book about WSS 3.0 with exercises and slides to help me get the basics of WSS 3.0.
That will keep me occupied for the next few days I think.

So that's about it for my first day of internship at Dolmen.
I'll update this blog on my findings and new assignments as much as possible so that maybe one day I'll be the
one making posts about how you make this or that instead of me browsing to other peoples blog for information :)