Finally...
I was certified in a lot of things but SharePoint. Today however I've finished my last SharePoint exam with succes. I've passed all WSS en Moss exams.
Up to the next ones!(Maybe the next round of MCM?)
Cheers,
Wes
Who’s doing what?
In my life time I’ve only met three good managers and they share some common characteristics which made them so good at what they are doing. In this post I’ll try to determine what it is that makes them that good while writing.
Directions
The first thing to get people to run in the right direction is to give them bearings. Which direction do we want to be running? A manager should be responsible for that and on the steering wheel at all times. If we leave it up to the young and eager developers we’ll all be running really hard, but probably in opposite directions. The first thing a manager needs to do is direct people in the right way. This cannot be forced however!
Leadership
If somebody is being force fed he’ll throw up eventually, leaving a big mess behind. The thing with good managers is that the people usually come to them to ask them for advise on how and what to do. It is a combination of both knowledge and a natural born leadership capacity that makes this work. You can not rest assured however that this will continue to work without effort.
Patience
People will only keep continue to ask their manager on what and how to do things as long as the manager is approachable and patience. A good manager cannot be out of office most of the day and should take time to really send somebody in the right direction. That takes skills.
Teaching
Teaching is one of those skills. A good manager will not give you the answer to your questions. It will ask you questions that will get you to your answer. A good manager is like an oracle. Teaching is not about using a red pen and pointing out what somebody is not good at. It is the opposite way around.
Appointments
Another thing that makes a manager a good manager is that he or she can delegate responsibilities without losing track. They’ll have to have authority to make people work for them and they should not be afraid to point out to someone that he or she is lacking behind. Again, this cannot be forced, it’s got to be something that comes naturally.
Bonding
The last thing I would like to mention is bonding. A good manager is the glue of a group of workers. All workers need to trust the manager to cover their backs if they do what they were told to do. A good manager can scent frustrations within a group, and knows when to act.
Conclusion
Of course this post isn’t complete at all. The three good managers I met are far more complicated and interesting than I can describe in one blog post. Please add to my list using the comments if you like.
Cheers,
Wes
P.S. I would really like to thank Karin van Klink, Fred van Luttikhuizen and Beat Nideröst.
I have to admit. I am late! Much to late! I promised you all a DownloadAsZip feature for SharePoint in a few days and it took me more than a month. But finally it’s here. First have a look at what it’s actually all about.
A picture says so much more
Hover the images for explanation…
How did we get to this result?
I’ve explained how to create the multi select button and how to create the download handler in two previous posts. So now I’m going to focus on the last part. The ‘DownloadAsZip’ buttons.
Easy peasy
Let’s start with the easiest part. With the handler in place we can add custom actions to list items simply by adding some CustomAction elements to our Elements manifest file. Here’s what the elements.xml looks like:
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
...
<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}&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}&Item={ItemId}" /> </CustomAction>
</Elements>
What this fragment does is that it adds two buttons to the edit control block. One for files (RegistrationId=”0x0101”) and one for folders (RegistrationId=”0x0120”). These custom actions simply redirect the client to the previously created DownloadAsZip.ashx with the correct query string values.
So this adds single item DownloadAsZip functionality to our SharePoint site.
Little bit more difficult
The code for the DownloadAsZip button on the ListView level is somewhat more difficult but still not to complicated. The code looks like this:
//-----------------------------------------------------------------------
// <copyright file="DownloadAsZipAction.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 DownloadAsZipAction : MenuItemTemplate { /// <summary>
/// Initializes a new instance of the <see cref="DownloadViewAsZipAction"/> class.
/// </summary>
public DownloadAsZipAction()
: base("Download as Zip", "/_layouts/images/ICZIP.GIF") { base.Description = "Download selected files or complete view if no items selected.";
}
/// <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) { ListViewWebPart listViewWebPart = FindListView(this.Parent);
if (listViewWebPart == null) { this.Visible = false;
}
if (this.Visible) { string navigateUrl = string.Format(CultureInfo.InvariantCulture,
"{0}/_layouts/DownloadAsZip.ashx?List={1}&View={2}&Item=", SPContext.Current.Web.Url,
listViewWebPart.ListName,
listViewWebPart.ViewGuid);
string clientClick = string.Format(CultureInfo.InvariantCulture,
"window.location = '{0}' + GetSelectedItemsString('WebPart{1}')", navigateUrl,
listViewWebPart.Qualifier);
this.ClientOnClickScript = clientClick;
}
base.Render(output);
}
protected ListViewWebPart FindListView(Control parent) { ListViewWebPart retVal = parent as ListViewWebPart;
if (retVal != null) { return retVal;
}
if (parent.Parent == null) return null;
return FindListView(parent.Parent);
}
}
}
The code is pretty straightforward. We create a class that inherits the MenuItemTemplate class and we change the ClientOnClickScript property in the render method. What the result of this is, is that client will be redirected to the DownloadAsZip.ashx with the selected items in the querystring. The javascript is explained in one of the previous posts.
To add this button to the ListView we need to adjust our elements.xml a little. We actually add two more CustomAction elements in there.
<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>
The second one is to add the ‘Enable item selection’ button and the first one to add the ‘Download as Zip’ button. Pay attention to the RegistrationId again. We add this button only to lists that inherit from the Document Library.
Wrapping it up
Well if you read all of the posts you’ve noticed that this is a pretty complex feature. We have a dependency on the jQuery feature. A lot of JavaScript. A custom HttpHandler etc. etc.
If you don’t know how to create solution packages yourself. Or if you don’t have the time to read all posts and create your own packages you can download the motion10 SharePoint Solution Pack. I’ve updated the Share Point Solution Pack to contain all these and and some more features.
!Please be smart and test the solution package in a test environment!
Cheers and have fun,
Wes
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
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.
- Where to leave the temporary files and when to cleanup?
- What will a client request for 3Gb. of data will do with my memory?
- 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.
- Register the handler in the Web.config
(we’ve edited the web.config before)
- 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
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!:
Oh no! Let’s go through the steps again.
The standard steps to perform
- Use STSadm to configure the use of delegation
- Create a Data Connection Library and Report Library
- Configure the SharedServiceProvider to trust my Data Connection Library
- Configure the SharedServiceProvider to trust my Report Library
- Create a Data Connection with Excel
- Export the Data Connection
- Save the Data Connection File to our trusted library and approve it
- Create an Excel sheet using the connection file from the library
- Design and publish the sheet to our trusted file location
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.
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!
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
Default a SharePoint list does not have an option to select multiple items and looks like this:
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):
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}&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}&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:
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
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:
And then use these parameters in our filter criteria:
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
Sometimes I’m amazed and today is such a rare moment. This should be embedded in Visual Studio 2010 by default!
Have a look over here and get amazed. Trust me it works!
Ever wandered about how it would be if you could validate the input of your clients with some regular expressions? Roaming the internet searching for a solution you do find some guys and girls who write about the fact that they created a Regular Expression field for SharePoint but they don’t explain how they did it. In this post I’ll explain to you how to create such a field. Which isn’t to difficult.
Think first, code later.
This is something that bothers me for a long time. SharePoint developers seem to be the copy and paste masters of this universe. They forget design principles, act like robots doing what they’ve been told and forget to be creative.
When reading examples on how to create custom fields I’m having trouble not to cry. Everybody starts with a Field, a FieldControl and an UserControl. No matter what kind of field, that’s how it should be done. Next they copy the files to the correct location and they have a new field. Perfect, right? WRONG! PLAIN WRONG!
Each developer knows that if he’s about to create a class, the first thing he should do is to check whether there might be an existing class that implements most of the features already. If so, we inherit that class and create some extra methods and or properties. In our case we are going validate a string input by matching it with a regular expression. And SharePoint does have a great solution for string input already. It’s called SPFieldText.
Get started with the SPFieldRegexMatch
So we need a class, that inherits from the SPFieldText and implements two extra properties: ValidationExpression and ErrorMessage:
using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
namespace Motion10.SharePoint2007 { /// <summary>
/// Field that validates it value by the given regular expression
/// </summary>
[Guid("92A8DAFE-92C5-407c-A1E6-7BF0C80FB904")] public class SPFieldRegexMatch : SPFieldText { /// <summary>
/// Initializes a new instance of the <see cref="SPFieldRegexMatch"/> class.
/// </summary>
/// <param name="fields">The fields.</param>
/// <param name="fieldName">Name of the field.</param>
public SPFieldRegexMatch(SPFieldCollection fields, string fieldName)
: base(fields, fieldName) { base.MaxLength = this.MaxLength;
}
/// <summary>
/// Initializes a new instance of the <see cref="SPFieldRegexMatch"/> class.
/// </summary>
/// <param name="fields">An <see cref="T:Microsoft.SharePoint.SPFieldCollection"></see> object that represents the field collection.</param>
/// <param name="typeName">A string that contains the name of the field type, which can be a string representation of an <see cref="T:Microsoft.SharePoint.SPFieldType"></see> value.</param>
/// <param name="displayName">A string that contains the display name of the field.</param>
public SPFieldRegexMatch(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName) { base.MaxLength = this.MaxLength;
}
/// <summary>
/// Gets or sets the maximum number of characters that can be typed in the field.
/// </summary>
/// <value></value>
/// <returns>
/// A 32-bit integer that specifies the maximum number of characters.
/// </returns>
new public virtual int MaxLength { get { string propVal = this.GetCustomProperty("MaxLength") + "";
int retVal;
if(int.TryParse(propVal, out retVal)){ return retVal;
}
return 0xff;
}
set { this.SetCustomProperty("MaxLength", value); base.MaxLength = value;
}
}
/// <summary>
/// Gets or sets the validation expression.
/// </summary>
/// <value>The validation expression.</value>
public virtual string ValidationExpression { get { return this.GetCustomProperty("ValidationExpression") + ""; } set { this.SetCustomProperty("ValidationExpression", value); } }
/// <summary>
/// Gets or sets the error message.
/// </summary>
/// <value>The error message.</value>
public virtual string ErrorMessage { get { string retVal = this.GetCustomProperty("ErrorMessage") + ""; ; if (string.IsNullOrEmpty(retVal)) { retVal = string.Concat(this.Title,
" does not match the regular expression: ",
HttpUtility.HtmlEncode(this.ValidationExpression));
}
return retVal;
}
set { this.SetCustomProperty("ErrorMessage", value); } }
/// <summary>
/// Used for data serialization logic and for field validation logic that is specific to a custom field type to convert the field value object into a validated, serialized string.
/// </summary>
/// <param name="value">An object that represents the value object to convert.</param>
/// <returns>
/// A string that serializes the value object.
/// </returns>
[SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
public override string GetValidatedString(object value) { string retVal = base.GetValidatedString(value);
string textValue = base.GetFieldValueAsText(value);
if (!string.IsNullOrEmpty(textValue) && !string.IsNullOrEmpty(this.ValidationExpression)) { Regex validationRegex = null;
try { validationRegex = new Regex(this.ValidationExpression);
}
catch (ArgumentException) { throw new SPFieldValidationException("The configured regular expression is not valid. Please contact an administrator of this list to correct the issue."); }
if (!validationRegex.IsMatch(textValue)) { throw new SPFieldValidationException(this.ErrorMessage);
}
}
return retVal;
}
}
}
As you can read in this code sample it’s all pretty straight forward. We use the GetCustomProperty and SetCustomProperty to save the SPFieldRegexMatch its properties and we override the GetValidatedString method to indeed validate our input.
Unfortunately SharePoint does NOT call the property its setter method so we can not validate the regular expression on input. Which is the reason why we validate the regular expression on validation.
We do have our custom field type but, how do we signal SharePoint that we have this lovely new field?
Continue with some CAML
SharePoint has its fields defined in xml files in de template/xml folder which names start with ‘fldtypes_’. All we need to do is create an xml file named 'fldtypes_motion10.xml' and define our custom field like so:
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<Field Name="TypeName">RegexMatch</Field>
<Field Name="ParentType">Text</Field>
<Field Name="TypeDisplayName">Single line of validated text</Field>
<Field Name="TypeShortDescription">Single line of validated text(regular expression validation)</Field>
<Field Name="UserCreatable">TRUE</Field>
<Field Name="Sortable">TRUE</Field>
<Field Name="AllowBaseTypeRendering">TRUE</Field>
<Field Name="Filterable">TRUE</Field>
<Field Name="FieldTypeClass">Motion10.SharePoint2007.SPFieldRegexMatch, SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a</Field>
<RenderPattern Name="DisplayPattern" Type="Text">
<HTML><![CDATA[<span title="Regular Expression Match field by motion10">]]></HTML>
<Column HTMLEncode="TRUE" />
<HTML><![CDATA[</span>]]></HTML>
</RenderPattern>
<PropertySchema>
<Fields>
<Field Name="MaxLength"
DisplayName="Max Length:"
Required="TRUE"
MaxLength="3"
Min="1"
Max="255"
DisplaySize="3"
Type="Integer">
<Default>255</Default>
</Field>
<Field Name="ValidationExpression"
DisplayName="Validation Expression:"
Required="TRUE"
MaxLength="500"
DisplaySize="35"
Type="Text">
<Default></Default>
</Field>
<Field Name="ErrorMessage"
DisplayName="Error Message:"
MaxLength="500"
DisplaySize="35"
Type="Text">
<Default></Default>
</Field>
</Fields>
</PropertySchema>
</FieldType>
</FieldTypes>
This xml is very straight forward as well. Just for fun I inserted a RenderPattern but that’s not necessary at all. Since we inherit from SPFieldText, we have our rendering templates already.
With the PropertySchema we define the editable properties for which the UI will be rendered automagically. So all that’s left to do is to create a new WSPBuilder solution, add the class and the xml file, built, deploy and we are finished!
Cheers and have fun,
Wesley
P.S. the ValidationExpression and ErrorMessage properties are declared virtual with a reason. It’s very simple to override those properties and create a new special field. Like SPFieldEmailAddress for example:
using System;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
namespace Motion10.SharePoint2007 { /// <summary>
/// Field that validates its value to check whether it is an email address
/// </summary>
[Guid("D306F9F2-2CF2-4ff3-BA4D-CC1C51126DCC")] public sealed class SPFieldEmailAddress : SPFieldRegexMatch { /// <summary>
/// Initializes a new instance of the <see cref="SPFieldEmailAddress"/> class.
/// </summary>
/// <param name="fields">The fields.</param>
/// <param name="fieldName">Name of the field.</param>
public SPFieldEmailAddress(SPFieldCollection fields, string fieldName)
: base(fields, fieldName) { }
/// <summary>
/// Initializes a new instance of the <see cref="SPFieldEmailAddress"/> class.
/// </summary>
/// <param name="fields">An <see cref="T:Microsoft.SharePoint.SPFieldCollection"></see> object that represents the field collection.</param>
/// <param name="typeName">A string that contains the name of the field type, which can be a string representation of an <see cref="T:Microsoft.SharePoint.SPFieldType"></see> value.</param>
/// <param name="displayName">A string that contains the display name of the field.</param>
public SPFieldEmailAddress(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName) { }
/// <summary>
/// Gets or sets the maximum number of characters that can be typed in the field.
/// </summary>
/// <value></value>
/// <returns>
/// A 32-bit integer that specifies the maximum number of characters.
/// </returns>
/// <remarks>The setter is not implemented.</remarks>
public override int MaxLength { get { return 255;
}
set { throw new NotImplementedException();
}
}
/// <summary>
/// Gets or sets the validation expression.
/// </summary>
/// <remarks>The setter is not implemented.</remarks>
public override string ValidationExpression { get { return @"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$"; }
set { throw new NotImplementedException();
}
}
/// <summary>
/// Gets or sets the error message.
/// </summary>
/// <value>The error message.</value>
/// <remarks>The setter is not implemented.</remarks>
public override string ErrorMessage { get { string retVal = string.Concat(this.Title,
" does not contain a valid email address.");
return retVal;
}
set { throw new NotImplementedException();
}
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<Field Name="TypeName">EmailAddress</Field>
<Field Name="ParentType">Text</Field>
<Field Name="TypeDisplayName">Email address</Field>
<Field Name="TypeShortDescription">Email address</Field>
<Field Name="UserCreatable">TRUE</Field>
<Field Name="Sortable">TRUE</Field>
<Field Name="AllowBaseTypeRendering">TRUE</Field>
<Field Name="Filterable">TRUE</Field>
<Field Name="FieldTypeClass">Motion10.SharePoint2007.SPFieldEmailAddress, SharePointSolutionPack, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a7cd02bdf107f7a</Field>
<RenderPattern Name="DisplayPattern" Type="Text">
<HTML><![CDATA[<span title="Email address field by motion10">]]></HTML>
<Column HTMLEncode="TRUE" />
<HTML><![CDATA[</span>]]></HTML>
</RenderPattern>
</FieldType>
</FieldTypes>
More Posts
Next page »