Wesley Bakker

Interesting things I encounter doing my job...

Sponsors

News

Wesley Bakker
motion10
Rivium Quadrant 151
2909 LC Capelle aan den IJssel
Region of Rotterdam
The Netherlands
Phone: +31 10 2351035

(feel free to chat with me)

Add to Technorati Favorites

March 2009 - Posts

BizTalk + SharePoint: 1+1=3

You probably wondered why my blog has been quiet the last two weeks. Well, today my colleague Gijs in ‘t Veld and I published our white paper on BizTalk Server and SharePoint integration, describing best practices for providing a “face” to BizTalk Server. It describes three examples: Exception Handling, Manager Approval and BizTalk Dashboard.

We cover a lot of integration features of SharePoint so even if you are not interested in BizTalk at all, you might want to have a look at this whitepaper. It contains complete walkthroughs, tips and explanations,  on:

  • How to configure Excel Services to run in delegation mode
  • How to create and use Data Connection Libraries in SharePoint
  • How to integrate SharePoint, Excel and Analysis Services 2008
  • How to create and publish InfoPath forms using InfoPath Forms Services
  • How to integrate SharePoint task lists with Microsoft Outlook
  • How to create a workflow with SharePoint Designer
  • How to design large SharePoint lists
  • How to design for large SharePoint sites
  • How to design SharePoint authorization
  • And more!

You can download it here: http://www.motion10.com/best-practices/view.asp.

Cheers,

Wesley

DownloadAsZip SharePoint Feature Part II

Sometimes I get a very simple question which results in a not so simple solution. And this was one of those.

“How can I download multiple files and or folder from a SharePoint list at once?”

My first response was to use ‘explorer view’ but it lacks a lot of functionality. How about filters or views applied? As soon as you select the explorer view, all filters are dropped. How about cross browser support? Explorer view is available on IE only. So explorer view is more an ‘all or nothing IE only’ solution.

The idea for another SharePoint customization was born….

Two issues

Actually we’re dealing with two issues here. The first one is that we do not have a way to select multiple items from a SharePoint list view. The second one is to actually download those selected files.

I decided to really cut them loose. Because the feature of selecting multiple items could come in handy for other features than downloading as well. I can think of mailing them as attachment, deleting multiple items at once, send them to the repository etc. etc. etc.

First things first, so I solved the selection of multiple items in one of my previous posts. Today we’re going to create the code for our http handler.

Considerations

Just to be short. We do NOT want to create a file on the server and then send it to the client and we definitely do NOT want to write the zip file to a memory stream before sending it to the client either for multiple reasons.

  1. Where to leave the temporary files and when to cleanup?
  2. What will a client request for 3Gb. of data will do with my memory?
  3. What about timeouts if the creation of a file takes 3 minutes?

We have only one option left and that’s to create the zip stream on the fly and hook it to our response stream. We don’t have any timeout issues because the client will start to receive bytes right away. We don’t have any issues with file storage and cleanup and if we use a buffer of only 4k for our stream we don’t have any memory considerations left.

! Do keep in mind though that compacting a file does cost a lot of processing power. Unfortunately that’s not something we can solve. !

Http Handler

I can’t stress this out enough: Don’t use a Page to stream data to your client. Just don’t. It is an http handler indeed but it will introduce a lot of overhead for nothing. You should create your own class that implements the IHttpHandler interface. So that’s what we’ll do.

We need some query string variable in there for us to know which files to add to the zip. So let’s outline them:

  • List
    - required id of the list
  • View
    - optional id of the view. We’ll use the default view if no view is given.
  • Item
    - optional comma separated list of item ids. If no items are selected the complete view will be added to the zip.

So the start of the ProcessRequest method in our custom handler looks like this:

/// <summary>
/// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
/// </summary>
/// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
public void ProcessRequest(HttpContext context) {
    HttpResponse response = context.Response;
 
    string fileName = string.Concat(DateTime.Now.ToFileTimeUtc(), ".zip");
    string contentDisposition = string.Concat("attachment; filename=\"", fileName, "\"");
 
    response.Clear();
    response.ContentType = "application/zip";
    response.AddHeader("Content-Disposition", contentDisposition);
    response.Buffer = false;
 
    NameValueCollection queryString = context.Request.QueryString;
    string listId = queryString["List"];
    if (string.IsNullOrEmpty(listId)) {
        throw new WebException("Required query string parameter 'List' is not defined.");
    }
 
    string viewId = queryString["View"];
    string itemsString = queryString["Item"];

We clear the response stream and add the necessary headers before we send any content to the response stream. I decided to use DateTime.Now.ToFileTimeUtc() as the proposed filename to the client. We simply extract the query string parameters and check if the required List parameter is indeed there. If not we throw a WebException which SharePoint will graciously handle for us.

! What’s very important to notice here is that we set response.Buffer to false. In this way, everything we write to the response stream will be sent to the client right away. If we don’t set response.Buffer to false… we’ll have memory issues in no time. !

Zip Library

Although the .NET framework has a DeflateStream class to compress streams it still lacks the creation of zip files. In order to create a zip file you need to create file headers and stuff so I decided to use another library for it. First I went for the SharpZipLib but it has a GPL license which is not always permissible in a company so I looked around a little and there’s a very good alternative in the form of DotNetZip. Have a look at the conversation I had with one of the guys over here on how to solve some issues with it. They are really helpful! I didn’t have the time to rewrite my initial version of DownloadAsZip but I probably will do so in the future.

For now the code is based on the SharpZipLib and the rest of the ProcessRequest method looks like this:

SPWeb currentWeb = SPContext.Current.Web;
SPList selectedList = currentWeb.Lists[new Guid(listId)];
 
using (ZipOutputStream zipOutputStream = new ZipOutputStream(context.Response.OutputStream)) {
    zipOutputStream.SetLevel(9);
 
    if (!string.IsNullOrEmpty(itemsString)) {
        string[] items = itemsString.Split(',');
        foreach (string item in items) {
            int itemId;
            if (int.TryParse(item, out itemId)) {
                SPListItem listItem = selectedList.GetItemById(itemId);
                switch (listItem.FileSystemObjectType) {
                    case SPFileSystemObjectType.File:
                        zipOutputStream.AddSPFile(listItem.File);
                        break;
                    case SPFileSystemObjectType.Folder:
                        zipOutputStream.AddSPFolder(listItem.Folder);
                        break;
                    default:
                        throw new FileNotFoundException("No such file or folder.");
                }
            }
        }
    }
    else {
        SPView selectedView = selectedList.DefaultView;
        if (!string.IsNullOrEmpty(viewId)) {
            selectedView = selectedList.GetView(new Guid(viewId));
        }
 
        SPListItemCollection selectedItems = selectedList.GetItems(selectedView);
 
        zipOutputStream.AddSPListItemCollection(selectedItems);
    }
}
 
context.Response.End();
}

A few important things to notice.

In line 4 we create a ZipOutputStream and tie it to the OutputStream and on line five we set the compression level to the highest possible. Unfortunately there’s no documentation on what this does to performance and what’s the benefits in terms of compression ratio. It’s something we’ll still have to figure out.

And secondly we write files and folders to our (now zipped) response stream with the methods:

  • AddSPFile(SPFile)
  • AddSPFolder(SPFolder)
  • AddSPListItemCollection(SPListItemCollection)

Extension Methods

The guys of the SharpZipLib did not implement the methods to add SharePoint files to a ZipOutputStream. That’s something we have to do ourselves. I started with a few static methods to add items and I found myself passing the ZipOutputStream object continuously from one method to the other. With extension methods this is something we can refactor to a very nice and clean solution with virtualy no extra effort. So here’s the code for the accompanying ZipUtility class:

//-----------------------------------------------------------------------
// <copyright file="ZipUtility.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.SharePoint;
 
namespace Motion10.SharePoint2007.Zip {
    public static class ZipUtility {
        /// <summary>
        /// Adds the SPListItemCollection to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="listItemCollection">The list item collection.</param>
        public static void AddSPListItemCollection(this ZipOutputStream zipOutputStream, SPListItemCollection listItemCollection) {
            foreach (SPListItem listItem in listItemCollection) {
                switch (listItem.FileSystemObjectType) {
                    case SPFileSystemObjectType.File:
                        zipOutputStream.AddSPFile(listItem.File);
                        break;
                    case SPFileSystemObjectType.Folder:
                        zipOutputStream.AddSPFolder(listItem.Folder);
                        break;
                }
            }
        }
 
        /// <summary>
        /// Adds the SPFolder to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="folder">The folder.</param>
        public static void AddSPFolder(this ZipOutputStream zipOutputStream, SPFolder folder) {
            zipOutputStream.AddSPFolder(folder, string.Empty);
        }
 
        /// <summary>
        /// Adds the SPFolder to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="folder">The folder.</param>
        /// <param name="path">The path.</param>
        public static void AddSPFolder(this ZipOutputStream zipOutputStream, SPFolder folder, string path) {
            path = Path.Combine(path, folder.Name);
 
            ZipEntry entry = new ZipEntry(path + "/");
            zipOutputStream.PutNextEntry(entry);
 
            foreach (SPFile file in folder.Files) {
                zipOutputStream.AddSPFile(file, path);
            }
 
            foreach (SPFolder subFolder in folder.SubFolders) {
                AddSPFolder(zipOutputStream, subFolder, path);
            }
        }
 
        /// <summary>
        /// Adds the SPFile to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="file">The file.</param>
        public static void AddSPFile(this ZipOutputStream zipOutputStream, SPFile file) {
            zipOutputStream.AddSPFile(file, string.Empty);
        }
 
        /// <summary>
        /// Adds the SPFile to the zip output stream.
        /// </summary>
        /// <param name="zipOutputStream">The zip output stream.</param>
        /// <param name="file">The file.</param>
        /// <param name="path">The path.</param>
        public static void AddSPFile(this ZipOutputStream zipOutputStream, SPFile file, string path) {
            string filePath = Path.Combine(path, file.Name);
 
            ZipEntry entry = new ZipEntry(filePath);
            entry.DateTime = file.TimeCreated;
            zipOutputStream.PutNextEntry(entry);
 
            using (Stream fileStream = file.OpenBinaryStream()) {
                byte[] buffer = new byte[4096];
                int sourceBytes;
                do {
                    sourceBytes = fileStream.Read(buffer, 0, buffer.Length);
                    zipOutputStream.Write(buffer, 0, sourceBytes);
                } while (sourceBytes > 0);
            }
        }
    }
}

 

Please take a look at the code carefully. What’s really important to notice here is that we always add one file at a time and while doing so we always have just one file stream opened inside a using statement(see AddSPFile method). Another important thing to notice is that we use a small buffer of only 4k. This is to make sure we don’t create a huge memory monster or choke SharePoint with many open file streams.

Implementing the handler

We have the code for the handler now but we have not yet registered this handler with IIS. There are two way to do so.

  1. Register the handler in the Web.config
    (we’ve edited the web.config before)
  2. Create an ashx file and add it to the _layouts directory

This time I decided to go for the last option. Because it has some minor advantages. The content of the DownloadAsZip.ashx file looks like this:

<%@ Assembly Name="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"%>
<%@ WebHandler Language="C#" Class="Motion10.SharePoint2007.DownloadAsZipHandler" %>

This of course doesn’t contain any real code as that’s all in our DownloadAsZipHandler class.

Conclusion

I know I promised a complete solution in my previous post but you guys and girls have to wait a few more days for the button. This post will be way to long if I’ll go through the code for the DownloadAsZip custom action as well. Just bare with me for a few more days.

And please DO leave comments if you like or dislike anything you read on this blog. I’m always in for improvements.

Cheers and have fun,

Wes

Solving the “Data Refresh Failed” error message.

I’m pretty convenient with SharePoint and MOSS but sometimes get bitten in the rear if I try to do something on autopilot. While working on a presentation about the combination of SharePoint and BizTalk I wanted to insert an Excel sheet in SharePoint to show some BAM data. This is simply a cube on analysis services. After doing all the stuff I normally do I expected all to work but BOOM!:

Data Refresh Failed

Oh no! Let’s go through the steps again.

The standard steps to perform

  1. Use STSadm to configure the use of delegation
    SetDelegation
  2. Create a Data Connection Library and Report Library
    CreateLists 

  3. Configure the SharedServiceProvider to trust my Data Connection Library
    TrustDataConnectionLibrary
  4. Configure the SharedServiceProvider to trust my Report Library
    TrustFileLocation
  5. Create a Data Connection with Excel
    CreateDataConnection
  6. Export the Data Connection
    ExportDataConnection
  7. Save the Data Connection File to our trusted library and approve it
    SaveDataConnection
  8. Create an Excel sheet using the connection file from the library
    SetDataConnection
  9. Design and publish the sheet to our trusted file location
    PublishedSheet 

But still it doesn’t work. I can use the connection from the library in Excel. I can view the sheet, but I can’t update the connection!

How come it doesn’t work!

Viewed the SharePoint logs. Nothing in there. Viewed the event viewer. Nothing in there.

Hmmm… lets use SQL server profiler to see if we actually get to login to the database. Strange, I don’t see a trace for Audit login when I try to connect. Ok this tells us that it’s a SharePoint issue at first.

Ok, maybe it’s permissions. Allowed all authenticated users “Full control” to both the data connection library and the excel sheet but this is not the issue. Didn’t think it would be cause View rights are enough but ok. What else?

I forgot something!

Going through a walkthrough can sometimes lead you to a forgotten step. If you decide to do a walkthrough please don’t be ignorant and perform and check each and every step and don’t skip one because you think you’ve done it correctly. The walkthrough I used is the Plan external data connections for Excel Services its a great resource which explains a lot of the details of Excel Services. I almost decided to skip the first step but fortunately I didn’t.

The first step tells you to go to the ‘Trusted data providers’ section and add your provider. I never had to do that before because this library contains most of the standard data providers already.

DefaultTrustedDataProviders

This time however I was developing all this on Windows Server 2008 with SQL Server 2008 and as you can see in one of the images above(step 6 and 8) we use the MSOLAP.4 provider and that’s not in there!!!

After searching for all this time it was just a matter of adding the data provider name to the trusted data provider library and it al worked like a charm!

AddTrustedDataProvider

 

 

Conclusion

I do hope this story / walkthrough is of any help to y’all. I took me quite some time to figure out. Next time I’ll definitely take all steps in a walkthrough!

Cheers,

Wes

Select Multiple List Items in SharePoint Feature

Default a SharePoint list does not have an option to select multiple items and looks like this:

List

Today I’m going to show you how to create a feature that enables the selection of multiple list items to make it look like this(notice the checkboxes):

SelectableList

JavaScript

I decided to go with some jQuery to adjust the list on the client side and one action button that simply adds or removes the checkboxes from the list. It’s all not to difficult if we use jQuery. So here’s the JavaScript:

//-----------------------------------------------------------------------
// <copyright file="ListItemSelection.js" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
function CreateParentInputCheckBox(webPartId) {
    return $("<th nowrap scope='col' class='ms-vh2'></th>").append(
                $("<input type='checkbox' title='(de)select all items' />").attr("id", webPartId + "0")
                .click(function() {
                    var checked = $(this).attr("checked");
                    $("[id^=" + webPartId + "_]").attr("checked", checked);
                })
            );
}
 
function CreateChildInputCheckBox(webPartId, itemId) {
    return $("<td></td>").append(
                $("<input type='checkbox' />").attr("id", webPartId + "_" + itemId)
                                              .val(itemId)
                                              .click(function() {
                                                  $("#" + webPartId + "0").attr("checked", $(this).attr("checked") && $("[id^=" + webPartId + "_]:not(:checked)").length == 0);
                                              })
            );
}
 
function AddCheckBoxesToListView(webPartId) {
    $("#" + webPartId + " table.ms-listviewtable>tbody")
            .find(">tr.ms-viewheadertr").prepend(CreateParentInputCheckBox(webPartId)).end()
            .find(">tr:not(.ms-viewheadertr)")
                .each(function() {
                    var itemId = $(this).find("td.ms-vb-title>table[id]").attr("id");
                    if (itemId) {
                        $(this).prepend(CreateChildInputCheckBox(webPartId, itemId));
                    }
                });
}
 
function IsSelectable(webPartId) {
    var selectableItems = $("#" + webPartId + " table.ms-listviewtable>tbody>tr:not(.ms-viewheadertr)>td.ms-vb-title>table[id]").length;
    return selectableItems > 0;
}
 
function RemoveCheckBoxesFromListView(webPartId) {
    $("[id^=" + webPartId + "_], #" + webPartId + "0").parent().remove();
}
 
function GetSelectedItemsString(webPartId) {
    var selectedIds = new Array();
    $("[id^=" + webPartId + "_]:checked")
        .each(function() {
                selectedIds.push($(this).val());
        });
 
    return selectedIds.join(",");
}
 
function ListItemSelection_ButtonClick(senderId, webPartId) {
    //jQueryon mozilla does not work with namespaces. We have to work with plain old javascript here...
    var sender = document.getElementById(senderId);
 
    if (sender.getAttribute("remove")) {
        RemoveCheckBoxesFromListView(webPartId);
        sender.setAttribute("text" ,"Enable item selection");
        sender.setAttribute("description", "Enable the selection of items.");
        sender.removeAttribute("remove");
    } else {
        AddCheckBoxesToListView(webPartId)
        sender.setAttribute("text", "Disable item selection")
        sender.setAttribute("description", "Disable the selection of items.");
        sender.setAttribute("remove", true);
    }
}
 
function ListItemSelection_Init(senderId, webPartId) {
    if (!IsSelectable(webPartId)) {
        var sender = document.getElementById(senderId);
        sender.parentNode.removeChild(sender);
    }
}

We create this ListItemSelection.js file and add it to our layouts directory. I use WSPBuilder for it, but you can use whatever you like to use for it.

The JavaScript checks if there’s a title column with edit control block in the listview because this field will contain the id of the item. If not, the button is removed because we can’t do anything without an id.

The button

“Which button?” you ask. We have to create a custom action button. We do need to have a custom action that registers the above mentioned JavaScript file and a startup script to verify we have a title column with edit control block. This isn’t that difficult at all. The code looks like this:

//-----------------------------------------------------------------------
// <copyright file="SelectItemsAction.cs" company="motion10">
//     Copyright (c) motion10. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
using System.Globalization;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;
 
namespace Motion10.SharePoint2007 {
    public class SelectItemsAction : MenuItemTemplate {
        /// <summary>
        /// Initializes a new instance of the <see cref="DownloadViewAsZipAction"/> class.
        /// </summary>
        public SelectItemsAction()
            : base("Enable item selection", "/_layouts/images/motion10/ListItemSelection.gif") {
            base.Description = "Enable the selection of items.";
        }
 
        /// <summary>
        /// Raises an event after the control is loaded but prior to rendering.
        /// </summary>
        /// <param name="args">An <see cref="T:System.EventArgs"></see> object that contains the event data.</param>
        [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
        protected override void OnPreRender(System.EventArgs args) {
            base.OnPreRender(args);
            if (this.ListViewWebPart == null) {
                return;
            }
 
            if (!Page.ClientScript.IsClientScriptIncludeRegistered("ListItemSelection")) {
                Page.ClientScript.RegisterClientScriptInclude("ListItemSelection", "/_layouts/ListItemSelection.js");
            }
 
            string startupScript = string.Format(CultureInfo.InvariantCulture,
                                                "$(function(){{ListItemSelection_Init('{0}', 'WebPart{1}');}});",
                                                this.ClientID,
                                                this.ListViewWebPart.Qualifier);
 
            Page.ClientScript.RegisterStartupScript(typeof(SelectItemsAction), this.ClientID, startupScript, true);
        }
 
        /// <summary>
        /// Sends the content of the control to the specified <see cref="T:System.Web.UI.HtmlTextWriter"></see> object, which writes the content that is rendered on the client.
        /// </summary>
        /// <param name="output">The HtmlTextWriter object that receives the server control content.</param>
        [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
        protected override void Render(System.Web.UI.HtmlTextWriter output) {
            if (this.ListViewWebPart == null || this.ListViewWebPart.ViewType != ViewType.Html) {
                this.Visible = false;
            }
 
            if (this.Visible) {
                string clientScript = string.Format(CultureInfo.InvariantCulture,
                                                    "ListItemSelection_ButtonClick('{0}', 'WebPart{1}')",
                                                    this.ClientID,
                                                    listViewWebPart.Qualifier);
 
                this.ClientOnClickScript = clientScript;
            }
 
            base.Render(output);
        }
 
        private bool searchedForListView = false;
        private ListViewWebPart listViewWebPart;
        private ListViewWebPart ListViewWebPart {
            get {
                if (!searchedForListView) {
                    listViewWebPart = FindListView(this.Parent);
                }
 
                return listViewWebPart;
            }
        }
 
        private static ListViewWebPart FindListView(Control parent) {
            ListViewWebPart retVal = parent as ListViewWebPart;
            if (retVal != null) {
                return retVal;
            }
 
            if (parent.Parent == null) return null;
 
            return FindListView(parent.Parent);
        }
    }
}

With this code we have a custom action class but it’s not bound to any list toolbar yet. That's what we'll do next.

The feature

In order to have this button available on lists we need to add a CustomAction to our features Elements.xml file. In my case I’m going to use the selected items to download them as a zip file. So I added this custom action to the "Download as Zip" feature. You can however use this custom action for any feature you can think of. Such as a DeleteMultipleItemsAtOnce feature. The elements.xml file looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction
    Id="{DE394AD0-0A8E-4e5c-B246-A498BA2A7FB2}"
    Title="Download as Zip"
    RegistrationType="List"
    RegistrationId="101"
    Location="Microsoft.SharePoint.StandardMenu"
    GroupId="ActionsMenu"
    ControlAssembly="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"
    ControlClass="Motion10.SharePoint2007.DownloadAsZipAction">
  </CustomAction>
  <CustomAction
    Id="{CB4BE13C-C095-4a02-B875-787325045759}"
    Title="Enable item selection"
    RegistrationType="List"
    RegistrationId="101"
    Location="Microsoft.SharePoint.StandardMenu"
    GroupId="ActionsMenu"
    ControlAssembly="SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a"
    ControlClass="Motion10.SharePoint2007.SelectItemsAction">
  </CustomAction>
  <CustomAction
    Id="{18A0608F-7917-4fa3-8164-18E81B55A551}"
    ImageUrl="/_layouts/images/ICZIP.GIF"
    Title="Download as Zip"
    Description="Download all files in this folder and view as one zip"
    RegistrationType="ContentType"
    RegistrationId="0x0120"
    Location="EditControlBlock">
    <UrlAction Url="{SiteUrl}/_layouts/DownloadAsZip.ashx?List={ListId}&amp;Item={ItemId}" />
  </CustomAction>
  <CustomAction
    Id="{175D475D-C962-4965-9C9B-7CAFBB36A669}"
    ImageUrl="/_layouts/images/ICZIP.GIF"
    Title="Download as Zip"
    Description="Download this file as zip"
    RegistrationType="ContentType"
    RegistrationId="0x0101"
    Location="EditControlBlock">
    <UrlAction Url="{SiteUrl}/_layouts/DownloadAsZip.ashx?List={ListId}&amp;Item={ItemId}" />
  </CustomAction>
</Elements>

As you can see we’ve added this custom action as the second element in our DownloadAsZip features Elements.xml file. In this particular case we've tight this action to document library lists. That's because we do not have any other feautres yet that use the selected items.

Conclusion

Once deployed we have this extra button that toggles item selection:

EnableSelection

DisableSelection

In my next post I’ll describe the DownloadAsZip feature which will contain a complete WSP file and source again which includes the SelectItemsAction. That feature uses the selected items to create a zip file.

Cheers and have fun!

Wes

Posted: Mar 05 2009, 01:30 PM by webbes | with 33 comment(s)
Filed under: ,
Finalizing the Virtual Earth web part

I promised you all I'd be back with the JavaScript to finish the Virtual Earth web part. It took me a little bit longer because a colleague of mine had a great suggestion actually. He wondered if the map could maintain state on post back AND if he could then use this state values to filter lists. In that way he could filter a list of locations simply by dragging around the Virtual Earth map control. That sounded like a great feature to me so, I decided to implement this feature.

The problem is however that all this has become pretty big and complicated. Explaining all this is not really interesting and would be a very long read. I promised you guys the source code however so I feel I have to do something.

So I decided to give you guys a brief overview of how it all fits together AND the complete source code in case you would like to really investigate how it all works.

Hidden Fields

The only way for me to maintain state is through hidden fields. So we had to register no less than 4 hidden fields on the page. These 4 hidden fields maintain the the minimum longitude, the maximum longitude, the minimum latitude, and the maximum latitude. With these four values I restore the virtual earth map control  by calling SetMapView

if (restorePosition) {
    var savedView = map.GetSavedPosition();
    if (savedView != null) {
        map.SetMapView(savedView);
    }
}

Initiate Postback

Saving these values isn’t that hard because you can attach an event with the Virtual Earth API but how to initiate a postback? We need a postback reference and some sort of trigger to call this function. I finished with a timer that get’s reset on every change and triggers after a given amount of time.

VEMap.prototype.ClearPostBackTimeout = function() {
    if (this.postBackTimeout) {
        clearTimeout(this.postBackTimeout);
    }
}
 
VEMap.prototype.SetPostBackTimeout = function() {
    this.ClearPostBackTimeout();
    this.postBackTimeout = setTimeout(this.postBackEventReference, this.autoPostBackTimeout);
}

SharePoint Designer

To use the values of our Virtual Earth control we need to get to SharePoint Designer and add parameters to our DataFormWebPart:

DataViewParameters

And then use these parameters in our filter criteria:

FilterCriteria

Do remember we filter our dataview web part with the values from our Virtual Earth web part. Unfortunately the dataview web part does not implement the ITableProvider interface or we could actually get the locations to display on the Virtual Earth webpart from our list. That would be extremely nice… maybe I will think of something to solve this.

The download

The download contains all the source files you need. And a WSP ready to be installed right away. Notice that you’ll need to have two features. The jQuery feature and the Virtual Earth feature. The Virtual Earth feature has a dependency on the jQuery feature.

DISCLAIMER: The download is as is. There’s no warranty what so ever and if you prefer to use it, you do so at your own risk. You should look at this code as en example of how things could be done and not necessarily how things should be done.

Conclusion

 

I do hope you all like this motion10 Virtual Earth feature and that it will serve your needs. If you have any questions, like to submit improvements or get in contact with me, please feel free to add a comment. You can also contact me by clicking the Live Messenger button in the left sidebar of this page.

Cheers,

Wes

More Posts