Archives for: October 2007

Becky
10/29/07

VSeWSS 1.1 CTP

Since I've focused several of my articles on this blog on the process of manually creating and editing manifest, feature, and element XML files, I thought I should point out that it will be easier to work with these files in the future, thanks to the Visual Studio extensions for Windows SharePoint Services. Although the 1.0 version of this has been around since March, in August, Alex Malek of the SharePoint Designer team announced version 1.1 CTP. Unfortunately this is not available with VS 2008 (aka Visual Studio codename "Orcas"), but it's supposed to ship by year's end, when VS 2008 is officially released.

You can read about the new release on the SharePoint Designer team blog in this post. You can download the CTP for yourself here.

The main new feature of this new release is the "WSP View" tool pane in Visual Studio. Keep in mind that you'll still need a working knowledge of how the XML files work together, but with this added functionality, it will make it a lot easier to modify these files, since they're automatically generated for you. Also, it's easier to understand how they're packaged in their WSP file because you can see the phsycial structure of the files.

I downloaded it and tried using it myself, and found it very helpful. After I installed it, I went into Visual Studio 2005 and said Create New Project. I got a list of the SharePoint templates available to me from within Visual Studio. Create New Project

I selected WebPart as my project template. This is what it produced for me:

Solution

As you can see, it created the web part's .cs file and the .webpart file inside a folder, and created a temporary key. It also added the appropriate SharePoint dll references.

Here you can see the WSP View of the project:

WSP View

Note that when you use the WSP view, you can open up those files and edit them right from the WSP view, in the same way you would open up the files in the Solution Explorer.

Here you can see the feature.xml file it automatically generates:

WSP View

And finally, when you are finished, you can deploy your solution directly from the Visual Studio IDE Build menu:

Build Menu

It's very common to need to apply a custom master page to your SharePoint site. Very frequently you need to apply your custom master page to all sites within your site collection. And perhaps you'll even need to apply your master page to all the site collections contained within all your web applications operating within a farm. This is easy enough to accomplish when you are customizing a site for the first time, since most subwebs are set up to inherit from their parent site, so if you customize the parent site, it will apply to all the children. But what happens when a site collection has already been operational, and you want to create a new custom page that applies to all the children sites, even if someone has said they didn't want to inherit from the parent site? Furthermore, what happens when you deactivate the feature? By deactivating the feature does that mean that you have to reset all the sites back to the "default.master" master page, thereby erasing all the customizations people had made? I have seen a number of articles out there about creating a custom master page and installing it as a feature, but I wanted to take it a step further and write code that will handle applying the master page to all children sites, as well as insuring that the feature could be successfully uninstalled and revert all the sites back to their original master page. (Only if the originally set master page can't be found will it revert the site to the default.master page.)

Before we begin, the first thing is to note the use of SPWeb.MasterUrl and SPWeb.CustomMasterUrl. A fairly good explanation of the meaning of these two properties can be found on Jim Yang's blog on SharePointBlogs.com: http://www.sharepointblogs.com/jimyang/archive/2006/07/09/moss-2007-and-wss-3-0-master-page.aspx. Secondly, there's a good post about creating a feature that installs a custom master page on Paul Papanek's blog, at http://mindsharpblogs.com/PaulS/archive/2007/06/18/1903.aspx.

OK... on to the code. The first step is to create your SharePoint Visual Studio project to build a feature solution package. To see how to do this, read my blog entry /index.php/2007/10/04/email_document_from_a_document_libraryE-mailing a Document from a SharePoint Document Library. Go ahead and create the project, name it GlobalMasterPage, modify its XMl file to create the wsp and cab files, dd the .ddf and .Targets file, and create a key to digitally sign the project. Last but not least, create your feature folder called GlobalMasterPage. Wasn't that easy? Moving on...

Create a new file inside your TEMPLATE\FEATURES\GlobalMasterTemplate folder and call it MyCustom.master. Go ahead and create the custom master page you want from within SharePoint Designer, then copy and paste the markup into your Visual Studio master page, or else just copy the contents of default.master in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATES\GLOBAL directory on your drive. Note: don't open up the default.master page from the GLOBAL directory in SharePoint Designer; that file is very important to SharePoint and SPD might modify its contents.) Also, create a preview image (which the user will see when they try to select the master page within the SharePoint browser UI), and call it CutomizedMasterPreview.gif, and place it inside the GlobalMasterPage folder as well.

OK, now create a file called elements.xml inside your GlobalMasterTemplate folder. Add the following XML:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Module Url="_catalogs/masterpage" RootWebOnly="TRUE">
        <File Url="MyCustom.master"
            Name="MyCustom.master"
            Type="GhostableInLibrary"
            IgnoreIfAlreadyExists="FALSE">
            <Property Name="ContentType" Value="$Resources:cmscore,contenttype_masterpage_name;"/>
            <Property Name="PublishingPreviewImage"
            Value="~SiteCollection/_catalogs/masterpage/Preview Images/Custom/CustomizedMasterPreview.gif, ~SiteCollection/_catalogs/masterpage/Preview Images/Custom/CustomizedMasterPreview.gif" />
            <Property Name="Title" Value="MyCustom.master" />
        </File>
    </Module>
    <Module Url="_catalogs/masterpage/Preview Images/Custom" RootWebOnly="TRUE">
        <File Url="CustomizedMasterPreview.gif" Name="CustomizedMasterPreview.gif" Type="GhostableInLibrary">
            <Property Name="Title" Value="CustomizedMasterPreview.gif" />
        </File>
    </Module>
</Elements>

If we take a look at what it's doing, it's adding a page called MyCustom.master to the /_catalogs/masterpage folder. We're assigning it three values: a Content Type, a preview image, and a Name. The next thing we're doing is adding a file to a folder called "Custom", within the "Preview Images" folder. (If the folder doesn't already exist, it will be created.) We're giving that file a Name property as well.

The next step is create a Feature Receiver. This is compiled code that will get fired when the feature gets activated and deactivated. Create a file called FeatureReceiver.cs in the root of your project. Add the following code:

using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace GlobalMasterPage
{
    public class FeatureReceiver : SPFeatureReceiver
    {

        public override void FeatureInstalled(SPFeatureReceiverProperties properties) {}

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {}

        public override void FeatureActivated(SPFeatureReceiverProperties properties) {}

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {}
    }
}

The first thing to do is to create constants that point to the files we'll be working with. We'll be installing the custom master page in the same directory as the other site collection master pages, which is the /_catalogs/masterpage folder within SharePoint. (This is a virtual folder within the top level site; it doesn't exist on the file system anywhere.) Add the following constants to your class:

const string defaultMasterUrl = "/_catalogs/masterpage/default.master";
const string customizedMasterUrl = "/_catalogs/masterpage/MyCustom.master";
const string previewImageName = "CustomizedMasterPreview.gif";

Now replace the FeatureActivating method with the following code:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    SPSite site = properties.Feature.Parent as SPSite;
    if (site == null)
        return;

    SPWeb rootWeb = site.RootWeb;
    rootWeb.AllProperties["OldMasterUrl"] = rootWeb.MasterUrl;
    rootWeb.AllProperties["OldCustomMasterUrl"] = rootWeb.CustomMasterUrl;
    rootWeb.MasterUrl = customizedMasterUrl;
    rootWeb.CustomMasterUrl = customizedMasterUrl;
    rootWeb.Update();
    foreach (SPWeb subWeb in rootWeb.Webs)
    {
        ProcessSubWebs(subWeb, true);
    }
}

private void ProcessSubWebs(SPWeb web, bool isActivation)
{
    if (isActivation)
    {
        web.AllProperties["OldMasterUrl"] = web.MasterUrl;
        web.AllProperties["OldCustomMasterUrl"] = web.CustomMasterUrl;
        web.MasterUrl = web.Site.RootWeb.MasterUrl;
        web.CustomMasterUrl = web.Site.RootWeb.MasterUrl;
    }
    else
    {
        DeactivateWeb(web);
    }
    web.Update();

    foreach (SPWeb subWeb in web.Webs)
    {
        ProcessSubWebs(subWeb, isActivation);
    }
}

First, we're checkng to make sure the feature's parent (in this case, site collection, exists. If not, exit and do nothing. The next step is record what the current master and custom master pages are for the top level site. We'll add a property to the SPWeb to keep track of this information. Note: you do not want to use the Properties property of SPWeb, as this will only return a subset of properties. Instead, make sure you use AllProperties. By adding a property to the property bag, if it doesn't exist, it will be created, and if it already exists, it will be overwritten. After we've done that, we can reset the root level site's MasterUrl and CustomMasterUrl properties. You must call SPWeb.Update() or else the properties you added to the SPWeb will not be saved. Finally, after we've done that, we recursively iterate through all the child sites and point them to this top level site's master page. (When you are finished, if you look at a child's site's Site Settings page for selecting a master page, you'll see that the radio button is checked indicating it's inheriting its master page from its parent.)

Easy enough. The next step is to write the code to deactivate the feature. This is slightly more complicated. Whereas we're using the solution package to deploy the master page and preview image to the correct directory in the site collection, we'll have to manually reset all the subsites back to their original master page (after we've determined if that master page still exists), then manually delete the custom master page and preview image.

Replace the FeatureDeactivating method with the following code:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    SPSite site = properties.Feature.Parent as SPSite;
    if (site == null)
        return;

    SPWeb rootWeb = site.RootWeb;
    DeactivateWeb(rootWeb);
    rootWeb.Update();

    foreach (SPWeb subWeb in rootWeb.Webs)
    {
        ProcessSubWebs(subWeb, false);
    }

    if (rootWeb.MasterUrl != customizedMasterUrl)
    {
        try
        {
            bool fileExists = rootWeb.GetFile(customizedMasterUrl).Exists;
            SPFile file = rootWeb.GetFile(customizedMasterUrl);
            SPFolder masterPageGallery = file.ParentFolder;

            SPFolder temp = masterPageGallery.SubFolders.Add("Temp");
            file.MoveTo(temp.Url + "/" + file.Name);
            temp.Delete();

            fileExists = masterPageGallery.SubFolders["Preview Images"].SubFolders["Custom"].Exists;
            SPFolder customFolder = masterPageGallery.SubFolders["Preview Images"].SubFolders["Custom"];
            if (customFolder.Files.Count == 1 && customFolder.Files[0].Name == previewImageName)
            {
                masterPageGallery.SubFolders["Preview Images"].SubFolders["Custom"].Delete();
            }
            else if (customFolder.Files.Count > 1 && customFolder.Files[previewImageName].Exists)
            {
                customFolder.Files[previewImageName].Delete();
            }
        }
        catch (ArgumentException)
        {
            return;
        }
    }
}

private void DeactivateWeb(SPWeb web)
{
    if (web.AllProperties.ContainsKey("OldMasterUrl"))
    {
        string oldMasterUrl = web.AllProperties["OldMasterUrl"].ToString();
        try
        {
            bool fileExists = web.GetFile(oldMasterUrl).Exists;
            web.MasterUrl = oldMasterUrl;
        }
        catch (ArgumentException)
        {
            web.MasterUrl = defaultMasterUrl;
        }

        string oldCustomUrl = web.AllProperties["OldCustomMasterUrl"].ToString();
        try
        {
            bool fileExists = web.GetFile(oldCustomUrl).Exists;
            web.CustomMasterUrl = web.AllProperties["OldCustomMasterUrl"].ToString();
        }
        catch (ArgumentException)
        {
            web.CustomMasterUrl = defaultMasterUrl;
        }

        web.AllProperties.Remove("OldMasterUrl");
        web.AllProperties.Remove("OldCustomMasterUrl");
    }
    else
    {
        web.MasterUrl = defaultMasterUrl;
        web.CustomMasterUrl = defaultMasterUrl;
    }
}

The first thing to do, as before, is make sure the parent site collection exist. Next, we'll deactivate the top level site. Inside DeactivateWeb, what we'll do is see if the "OldMasterUrl" property still exists in the web's properties collection. If it doesn't we have no alternative than to revert back to default.master. If we do find the OldMasterUrl property, check to make sure that the file it pointed to before still exists. (Note: although it would seem intuitive that accessing the Exists property on an SPFile object would return True or False, in fact, if a file doesn't exist, it throws an ArgumentException error. Also, you can't just call SPFile.Exists becuase it thinks you're trying to set it's value. That's why I created a dummy boolean, simply as an excuse to trigger the error if the file (or SPFolder) doesn't exist.) We follow the same exercise for the OldCustomMasterUrl property, (although we're not explictly checking for existence. It seems safe to me if one of our custom properties are there, the other will be as well.) If one or both original master pages don't exist in SharePoint any more at their old location, we revert back to default.master. Finally, we delete our custom properties because they are no longer needed. We then iterate through this same process for every child site.

Going back to our DeactivateFeature method, the next step is to delete our custom master page and icon from the /_catalogs/masterpage directory. Unfortunately, there seems to be an issue in SharePoint where you can not delete a master page that was installed by a feature, even if no one is referencing that master page. It will throw an error saying, "This item cannot be deleted because it is still referenced by other pages." Luckily, I found a quick, although hackish, work around on "C-Dog's .NET Tip of the Day" blog at http://www.dotnettipoftheday.com/Blog.aspx?Id=376. You have to move the page to another folder, then delete that folder. Also, we installed the preview image to a new folder called "Custom". Although the foler is meant to house our custom image, it's possible someone else could have added more content to it since it was created. If there's nothing other than our custom image in it, delete the folder (along with the image); otherwise, just delete our image out of it. And that's it... the feature receiver class is done.

Now it's time to create the feature manifest. Create a file called feature.xml inside your GlobalMasterPage folder. Add the following XML:

<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
    Id="400D8B01-1F64-46b4-A1DD-A1DA6A0E8E94"
    Title="Custom Master Page"
    Hidden="FALSE"
    Scope="Site"
    Version="1.0.0.0"
    ReceiverAssembly="GlobalMasterPage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a56f42ad8ea54585"
    ReceiverClass="GlobalMasterPage.FeatureReceiver">
    <ElementManifests>
        <ElementManifest Location="elements.xml" />
        <ElementFile Location="MyCustom.master" />
        <ElementFile Location="CustomizedMasterPreview.gif"/>
    </ElementManifests>
</Feature>

Several things to note: the Scope is "Site", which means it's applied at the Site Collection level. Secondly, notice that we are referencing our FeatureReceiver class, so that it's fired when the feature is installed or uninstalled. You can also see that the custom master page and preview image files are being included in the ElementManifests section, along with the elements.xml manifest file itself.

Now it's time to add the manifest file. It should look like this:

<Solution xmlns="http://schemas.microsoft.com/sharepoint/"
    SolutionId="AAB93B39-E127-4b90-B966-E6DC60D06C76"
    DeploymentServerType="WebFrontEnd"
    ResetWebServer="TRUE">
    <Assemblies>
        <Assembly DeploymentTarget="GlobalAssemblyCache" Location="GlobalMasterPage.dll">
            <SafeControls>
                <SafeControl Namespace="GlobalMasterPage" TypeName="*" Safe="True" />
            </SafeControls>
        </Assembly>
    </Assemblies>
    <FeatureManifests>
        <FeatureManifest Location="GlobalMasterPage\feature.xml"/>
    </FeatureManifests>


Looking at this, we can see that our DLL is being added to the GAC as well as the SafeControls node in the web.config, so it can be called and executed safely from within SharePoint. The manifest also points to the feature manifest.

Last but not least, modify your .ddf file so it looks like this:

.OPTION Explicit
.Set DiskDirectoryTemplate=CDROM
.Set CompressionType=MSZIP
.Set UniqueFiles=Off
.Set Cabinet=On

manifest.xml
bin\debug\GlobalMasterPage.dll GlobalMasterPage.dll

.Set DestinationDir=GlobalMasterPage
TEMPLATE\FEATURES\GlobalMasterPage\feature.xml
TEMPLATE\FEATURES\GlobalMasterPage\elements.xml
TEMPLATE\FEATURES\GlobalMasterPage\MyCustom.master
TEMPLATE\FEATURES\GlobalMasterPage\CustomizedMasterPreview.gif

Once you've finished compiling your code, you can load the .wsp file into SharePoint using the stsadm tool. When it's time to deploy the solution, you can either deploy it to every web application in the farm, or to a particular site collection. Once it has been deployed to a site collection, you can browse to that particular site collection and activate the feature. You should see all the pages now skinned with your new master page. (Note: you can't scope this feature for an entire farm, or even a web application, because the feature installs a master page into a particular top level site collection. If you try to scope it as "Farm", SharePoint will throw an error. That means, the way this code works, you can deploy the code to every web application, but you have to activate the feature for each site collection. You could probably write another feature that activates this feature for each site collection, but that's outside the scope of this blog post.)

You can download the source code here. Have fun and good luck.

A user posted the following question on the TechNet SharePoint discussion board:
We have a custom list that I need to create a workflow that will fire off an email message when a person is added to the list. Their email is captured in one of the columns. This would be my first workflow and I'm not sure where to start. Anyone point me in the right direction or share some code? Thanks.

Here's how you can do this in SharePoint Designer.

First off, the user said they're using a custom list that has an E-mail field. Here's a sample custom list, with a field called "E-mail":

Custom List

To create the workflow, open up SharePoint Designer and open up your SharePoint web site. Right click on the root of the site and select "New", then "SharePoint Content". Click on the Workflow tab and select "Blank Workflow" and press the OK button.

New Workflow

The first screen you come to says, "Define your new workflow." Give your workflow a name. Then select the list you want to apply the workflow to. (In the case of our example, it's "Employee Custom List". You have several options as to when the workflow gets kicked off. In this case, we'll say the workflow gets kicked off every time a list item changes. You can also say that it gets kicked off when an item first gets created.

Define Workflow

Once you have finished making your choices, click the "Next" button.

The first step is to decide what condition you're evaluating. In this case, we want to see if the E-mail field has been populated. To do this, we'll click on the "Condition" button, and select "Compare Employee Custom List field".

Compare Field

You will see the sentence "If field value equals value". Click on the "field" link, and a list of all the fields in the custom list will appear. (These include built in fields that you did not explicitly define on the list, such as Approval Statuas, Modified By, etc.) Select "E-mail" from the list.

Select Field

Next, click on the "Equals" link. Select "Is Not Empty" from the drop down list. Now you have your condition in place. Your sentence should read, "If E-mail is not empty." Now, we'll work on the Action statement. Click on the Actions button, and select "Send an E-mail" from the drop down list. You will see the sentence, "Email this messgae." Click on the "this message" link. A window will pop up that looks like an e-mail.

At this point, you can either type in your own static e-mail, and hard code which user or group you want to send the e-mail to, based on whom has been given rights to your site. In our scenario, let's send an e-mail to the person who just got added to the list. To do this, click on the address book icon to the right of the "To" field. You'll see your choice of users. In this case, we want to use a dynamic value. Click on "Workflow Lookup" and click the "Add" button.

Select Users

Another window will pop up, asking you to select the Souce and the field. Select "Current Item" from the first drop down list, and select "E-mail" from the Field list.

Lookup Details

Click the "OK" button, then click the "OK" button again. Now you should be back to your e-mail. We'll do a similar process for the subject of the e-mail. Click the symbol to the right of the subject line, select "Current Item" from the source, and then "Employee Name" from the list of fields, then click "OK". Finally, write a message in the body of the e-mail, such as "Your e-mail address was added to the Employee Custom List." Your sample e-mail should now look like this:

Email Configuration

Now add a second action below the action to send the e-mail. Select "Stop Workflow". (If this doesn't appear in your list, click the "More Actions..." link.) The sentence will appear, "then Stop the Workflow and log this message." Change the "this message" to say something like "Email Workflow completed." (Without adding a message, the workflow will not compile properly.) Click Finish and you are finished with your workflow!

Now try it out. Go to your list, and update an item in your list, by adding an e-mail address. The recipient's e-mail should look like this:

Result E-mail

By default, SharePoint Document Libraries enable a person to e-mail a link to a document in a document library. This makes sense, because from a security perspective, you want to make sure that whomever clicks on the link actually has access to view the document. SharePoint will take care of authentication, ensuring that person is allowed to view the document. However, someone on Microsoft's SharePoint discussion board presented an interesting scenario: their users don't have direct access to their SharePoint site over their network, so they wouldn't be able to open a link from an e-mail they sent to themselves, to get to that document, because they wouldn't have access to the SharePoint server URL. Intead, they wanted to be able to actually e-mail the document itself from the document library to themselves, so they could read it apart from being on the SharePoint site. I have written the code neceesary to install a Feature that will do just that. (Please note, I have not added extensive error handling, validation, multi-lingual resources, web.config values, etc., for the sake of brevity.)

The first step is to create a new project in Visual Studio. (I have used C# for this example.) I have named the project MailDocument. I will create a folder structure that contains a DeploymentFiles folder and a TEMPLATE folder. Inside the TEMPLATE folder I have two more folders: a FEATURES folder and a LAYOUTS folder, (mirroring the structure of the folders in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATES folder). Create a folder called MailDocument inside the FEATURES folder. Inside your project, add a reference to Microsoft.SharePoint. (You can browse to this dll, located in the ISAPI folder of the "12" directory.)

First, I want to create an ASPX page that will contain just a few controls: a TextBox for the user to enter the e-mail address they want to send the e-mail to, a Button control they can click to send the e-mail, and a Label control that will display a nice message to the user when it's finished sending, as well as Panel control that will hide the TextBox and the Button when the e-mail has been sent. Create a page called MailDocument.aspx inside the LAYOUTS folder.

This page is going to be an application page in SharePoint, (i.e. will live in the LAYOUTS directory in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12 folder), so it will use the application.master Master Page. I will fill two content placeholders on the page: one for the page title, and another for the page content.

<asp:Content ID="Title" runat="server" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea">
E-mail a Document
</asp:Content>

<asp:Content ID="ContentMain" runat="server" ContentPlaceHolderID="PlaceHolderMain">
<asp:Panel ID="MailDocumentPanel" runat="server">
Please enter the e-mail address you would like to send the document to:<br />
<asp:TextBox ID="EmailAddress" runat="server" Width="300"/>
<asp:Button ID="GoButton" runat="server" Text="Go" OnClick="GoButton_Click" />
</asp:Panel>
<asp:Label ID="SuccessMessage" runat="server"/>
</asp:Content>

The next step is to create a code-behind page for this page. Create a file called MailDocument.aspx.cs inside the FEATURES folder. Add the following "using" directives and add a namespace and class name as follows:

using System;
using System.IO;
using System.Net.Mail;
using System.Net.Mime;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.WebControls;

namespace MailDocument
{
    public class MailDocument : LayoutsPageBase
    {
    }
}

Now that we have a code-behind page, let's wire up the ASPX page to it. SharePoint can't reference code-behind pages the way a normal ASP.NET web applicatin does. In order for SharePoint to reference that code-behind page it will reference the class inside a compiled dll, and in order for the dll to be used by SharePoint, in needs to be trusted. One way to do that is to deploy it to the GAC. In order for a dll to be in the GAC, it needs to get signed. So the next step you'll want to take is to right click on the project properties, click on the "Signing" tab, and create a new key. After that, we'll want to extract the public key. (For a quick way to do this in Visual Studio, visit Andrew Connell's blog entry on the topic.) Now that we've done all that, we can wire the two pages together by adding the following line to the top of the ASPX page:

<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" Inherits="MailDocument.MailDocument, MailDocument, Version=1.0.0.0, Culture=neutral, PublicKeyToken=[Your token goes here]" %>

OK, now that we have front-end page built, let's write the code that will get the document and send the e-mail.

The page itself is going to live in the LAYOUTS directory, which all lists will reference. That being the case, we need to know which list, and which list item, we're going to be using. To do this, we'll pass the List GUID and the Item ID as parameters in the query string to the page. Once we've figured out which list item we're dealing with, we can grab the document file itself. The code looks as follows:

protected void GoButton_Click(object sender, EventArgs e)
{
    SPList list = SPContext.Current.Web.Lists[new Guid(Request.QueryString["ListId"])];
    SPDocumentLibrary docLib = (SPDocumentLibrary)list;
    int id = System.Convert.ToInt32(Request.QueryString["ItemId"]);
    SPListItem listItem = docLib.Items.GetItemById(id);
    if (listItem.File != null)
    {
        ContentPlaceHolder mainContentPlaceholder = this.Master.FindControl("PlaceHolderMain") as ContentPlaceHolder;
        string toEmailAddress = ((TextBox)mainContentPlaceholder.FindControl("EmailAddress")).Text.Trim();
        SendEmail(toEmailAddress, listItem.Title, listItem.File, list);
    }
}

The next thing we need to do is actually send the e-mail. We'll want to use the e-mail server that was configured in the SharePoint Central Administration. We'll find the name of the SMTP server, create an e-mail message, attach the file to the e-mail, and send it. Here's the method:

private void SendEmail(string toAddress, string subject, SPFile file, SPList list)
{
    SPWebApplication webApp = SPContext.Current.Site.WebApplication;
    SPOutboundMailServiceInstance mailServer = webApp.OutboundMailServiceInstance;
    string smtpServer = mailServer.Server.Address;

    string listUrl = SPContext.Current.Web.Url + list.DefaultViewUrl;

    MailMessage message = new MailMessage("administrator@linwareinc.com", toAddress);
    message.Subject = subject;
    message.IsBodyHtml = true;
    message.Body = "This file was sent from the following SharePoint list: " +
    "<a href=\"" + listUrl + "\">" + listUrl + "</a>.";

    Stream stream = file.OpenBinaryStream();
    //You can add code to figure out which MIME type to use based on the type of attachment.
    Attachment attachment = new Attachment(stream, @"application/msword");
    attachment.Name = file.Name;
    message.Attachments.Add(attachment);

    try
    {
        SmtpClient client = new SmtpClient(smtpServer);
        client.Send(message);
    }
    catch (Exception e)
    {
        throw e;
    }
    finally
    {
        stream.Close();
    }

    ContentPlaceHolder mainContentPlaceholder = this.Master.FindControl("PlaceHolderMain") as ContentPlaceHolder;
    ((Panel)mainContentPlaceholder.FindControl("MailDocumentPanel")).Visible = false;
    ((Label)mainContentPlaceholder.FindControl("SuccessMessage")).Text = "Your message has been sent.";
}

Now that we have the ASPX page put together, we need to start building the Solution package that contains the feature. The first step is to create a manifest file. Create a new XML file in the root of your project and call it manifest.xml. Add the following XML to it:

<Solution xmlns="http://schemas.microsoft.com/sharepoint/"
    SolutionId="72CFD5DA-10F0-499b-B4E9-863B6DB87717"
    ResetWebServer="TRUE">
    <FeatureManifests>
        <FeatureManifest Location="MailDocument\feature.xml"/>
    </FeatureManifests>
    <TemplateFiles>
        <TemplateFile Location="LAYOUTS\MailDocument.aspx"/>
    </TemplateFiles>
    <Assemblies>
        <Assembly DeploymentTarget="GlobalAssemblyCache" Location="MailDocument.dll" />
    </Assemblies>
</Solution>

This code is saying three things about the solution: 1. The feature manifest can be found inside the MailDocument folder in the FEATURES folder. 2. The MailDocument.aspx page is in the LAYOUTS folder. 3. Deploy the dll to the GAC.

The next step is to create the feature manifest. To do this, create an XML file called feature.xml inside your TEMPLATE/FEATURES/MailDocument folder in your project. Add the following XML to the file:

<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
    Id="5785441E-DD86-4df5-A867-D7AC37712A27"
    Title="Mail Document"
    Scope="Web" Hidden="false" Version="1.0.0.0"
    Description="Allows you to e-mail documents from your Document Libraries">
    <ElementManifests>
        <ElementManifest Location="elements.xml"/>
    </ElementManifests>
</Feature>

Notice that the Feature's Scope is "Web". That means this feature can be deployed for a particular subsite, meaning all the document libraries in a particular site will have this new functionality when the feature is deployed. The ElementManifest node is telling the feature where to find the element manifest file. So, last but not least, let's add the elements.xml file. To do this, create a file called elements.xml in the same directory you added the feature.xml file.

Although the elements manifest can be used to deploy files to a particular SharePoint library, we've already used the Manifest to instruct the solution package to deploy the ASPX page to the file system, and the dll to the GAC, so we don't need to deploy any more files. However, we do want our users to be able to get to the page they created. We do this by adding a "custom action", which in this case, will add an item to the context menu when a user right clicks on a list item in the document library. Add the following XML to the elements.xml file:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <CustomAction
        Id="MailDocument.Link"
        RegistrationType="List"
        RegistrationId="101"
        Location="EditControlBlock"
        Sequence="350"
        Title="E-mail Document"
        ImageUrl="~site/_layouts/IMAGES/EML16.GIF"
        Description="Allows a user to e-mail a document in a document library to themselves." >
        <UrlAction Url="/_layouts/MailDocument.aspx?ItemId={ItemId}&ListId={ListId}"/>
    </CustomAction>
</Elements>

Let's look at the XML. The RegistrationType attribute is telling us to add this action to a particular kind of list. The RegistrationId attribute is the Id for a Document library. Those two properties tell the action to be available on all lists that are Document Libraries. The Location attribute of "EditControlBlock" indicates the context menu that appears when the user right clicks an item in the library. The Title is what will appear in the context menu, and the ImageUrl indicates the location of the icon that will appear next to the item in the context menu (if you so choose to add one. In our case, we're using an image from the default SharePoint installation.) The UrlAction node indicates the URL the user should be taken to when the click the item in the context menu. Notice that the item id and list id can be passed using {ItemId} and {ListId} in the query string.

You've done it! You've created all the files necessary for your feature. The last thing we need to do is to compile the solution. In order to this, we'll need to take a few steps.

1.) Download the MAKECAB.EXE tool from Microsoft and install it in your WINDOWS\system32 directory. You can download the SDK at http://support.microsoft.com/default.aspx/kb/310618.
2.) Create a file inside the DeploymentFiles folder of your project and call it BuildSharePointPackage.Targets. Copy the following XML into that file:

<Project DefaultTargets="BuildSharePointPackage" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <MakeCabPath>"C:\Windows\System32\MAKECAB.EXE"</MakeCabPath>
    </PropertyGroup>
    <Target Name="BuildSharePointPackage">
        <Exec Command="$(MakeCabPath) /F DeploymentFiles\BuildSharePointPackage.ddf /D CabinetNameTemplate=$(MSBuildProjectName).wsp /D DiskDirectory1=wsp\$(Configuration) "/>
        <Exec Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU'"
Command="$(MakeCabPath) /F DeploymentFiles\BuildSharePointPackage.ddf /D CabinetNameTemplate=$(MSBuildProjectName).cab /D DiskDirectory1=wsp\$(Configuration)"/>
    </Target>
</Project>

Now we need to create a DDF file, which tells MAKECAB.EXE how to package the files we created. Create a new file called BuildSharePointPackage.ddf inside the DeploymentFiles folder. Add the following code to it:

.OPTION Explicit
.Set DiskDirectoryTemplate=CDROM
.Set CompressionType=MSZIP
.Set UniqueFiles=Off
.Set Cabinet=On

manifest.xml
bin\debug\MailDocument.dll MailDocument.dll
TEMPLATE\FEATURES\MailDocument\feature.xml MailDocument\feature.xml
TEMPLATE\FEATURES\MailDocument\elements.xml MailDocument\elements.xml
TEMPLATE\LAYOUTS\MailDocument.aspx LAYOUTS\MailDocument.aspx

The very last thing we need to do is to tell Visual Studio to create the CAB and WSP files (the WSP being what will ultimately be deployed to SharePoint) after the project compiles. To do this, you'll need to right click on the project and say "Unload project". (If you don't see this option, you'll need to change your Visual Studio settings so that you can see the VS Solution file.) After you have unloaded the project, right click the project and say "Edit MailDocument.csproj". This will pull up the XML file. Go to the very bottom of the file and look for the node that says "Import Project". Right after that node, and a second node that looks like this:

<Import Project="DeploymentFiles\BuildSharePointPackage.targets" />

At the very bottom of the page you'll see some node commented out. Replace the "AfterBuild" target with the following code (and uncomment it out):

<Target Name="AfterBuild">
    <CallTarget Targets="BuildSharePointPackage" />
</Target>

When you're done, your solution should look like this:

Now reload your project in Visual Studio and compile it. And voila! You should not only get your dll in the Debug directory, but you'll get a folder called wsp in your project root. (It's a hidden folder, so you'll have to toggle the Show All Files icon in your Solution Explorer pane.)

You can download a zip file of the code here.

After you have deployed the solution and activated the feature, let's take a look at the feature in action. Below we have a document library with a Word document in it. Below, you can see the context menu:

MailDocument context menu

When the user clicks on the link, they go to the .aspx page. Notice the URL in the browser, and how it has the list's guid and the item's id:

And here's the e-mail the person gets, including the attachment:

Parts of this code are based on code from a class I took last week by Andrew Connell, SharePoint MVP. You can view his blog at http://www.andrewconnell.com/blog/. The class was taken through the Ted Pattison Group. I highly recomment Andrew's course.

If you'd like to read more about how to do the kinds of things I've mentioned in this article, visit the following How To's on Microsoft MSDN site:
How to: Add Actions to the User Interface
Creating a Feature for an Entry Control Block Item in Windows SharePoint Services 3.0
Creating a Solution Package in Windows SharePoint Services 3.0

October 2007
Sun Mon Tue Wed Thu Fri Sat
 << < Current> >>
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      

Search

free blog tool