The Code

The source code for this post can be downloaded from CodePlex at:
http://www.codeplex.com/CLIWSSV3

Background

I was developing a multi-threaded SharePoint application that added a lot of items to a list during the course of testing. To reset my test environment, I had to go in and manually delete all of the list items. Even using the datasheet view, this got quite tedious and time consuming. I think I had over 10,000 items in the list at one point in the testing.

So, as is quite common for me, laziness at doing one task (clearing the list) prompted me into doing another task (creating a feature to clear list items).

My Requirements

  • My first requirement was that I needed the ability to clear out all of the list items in one shot. I did not want to have to keep clicking menu items to delete items from the list I wanted to clear.
  • I needed the functionality to be easy to get to. I wanted it to be accessible from SharePoint’s web interface, and I wanted to be able to access the clearing mechanism directly from the existing list menus.
  • I needed to be able to clear large lists. As I mentioned, at one point I had over 10,000 items in my list. This was particularly challenging because HTTP requests can timeout if there is no response back to the client.
  • I wanted a simple implementation and a simple deployment. I did not want to have to write a background service or anything crazy like that.

    In summary, I want an easily accessible menu item that lets me clear the contents of a list in a single shot, all operating from the web server. I don’t think that too much to ask for.

    Designing the Capability

    In order to meet all of my requirements I came up with the following use case. To clear a list, a user would navigate to one of the list’s views. This view would have the standard Actions menu available to all lists. The user would select the Clear List Items option from the Actions menu. The menu would redirect to a page that would run the code to delete the list items from the list. Once the page was finished, it would redirect back the list view page from which the user initiated the use case.

    The sounded pretty straight forward. The next part of my design focused on defining the SharePoint constructs that would implement the use case. I needed the menu item in the Actions menu. I knew the easiest way to get the menu there was with a Custom Action feature with a UrlAction pointing to my page. I needed an ASPX page that would start the processing of the clear list. I also wanted a compiled assembly that would perform the bulk of the work, for performance. Finally, I needed a SharePoint solution WSP (CAB) to make for an easy deployment.

    The Implementation

    The Deletion Engine

    The first component I created was the assembly that handles the item deletions using code. The assembly has a class with two static methods that take an SPList parameter. This is the list that will be cleared.

    Here is the code for the class:

    using System;

    using System.Collections.Generic;

    using System.Text;

    using Microsoft.SharePoint;

    namespace BlackBlade.ListActions.ClearList

    {

    public class ClearListAction

    {

    public static void ClearList(SPList ListToClear)

    {

    ClearList(ListToClear, ListToClear.Items.Count);

    }

    public static int ClearList(SPList ListToClear, int BatchSize)

    {

    int intDeletedItemCount = 0;

    while (ListToClear.Items.Count > 0 && intDeletedItemCount < BatchSize)

    {

    ListToClear.Items[0].Delete();

    intDeletedItemCount++;

    }

    return ListToClear.ItemCount;

    }

    }

    }

    The code was pretty simple. At first, I only had the first public method. Later, I added the second method because I found that when I called the first method from the web page, the page would timeout while processing a large list, a list with over 2,000 items in it.

    The ASPX Page

    Next, I created the ASPX page that initialized the site and list contexts and called the static methods from the assembly. I copied some ASPX tags from an existing default.aspx content page in the Template folder, and then cleared out everything but the barest essentials. I added a server-side code block to load the SharePoint site context and determine which list to clear based on some page parameters. Here are the parameters I chose for the page:

    • Source – This parameter that told the page where to redirect to once the list had been cleared.
    • ListUrl – The URL of the list. This was an optional parameter that would allow me to clear a list in another SharePoint site. I have not actually wired up anything to send this parameter to the page so I am thinking about removing it altogether.
    • List – This is the guid of the list that would be cleared.

    The Source parameter was always required as the page always had to know where to go after clearing the list was complete. At least one of the ListUrl or List parameters must be present as well, so the page would know which list to clear.

    The ASPX page translates the parameters into an SPWeb and SPList objects. The ASPX page passes the SPList object to one of the static methods in the assembly. At first I used the void ClearList(SPList ListToClear) override but then created the int ClearList(SPList ListToClear, int BatchSize) override. The second override allowed me to specify a batch size for item deletions. The method would delete a maximum of BatchSize items from the list and return execution to the page. The page would then redirect back to itself. This would create a new HTTP connection and resume the item deletions anew. Once all of the items had been deleted, the page would redirect to the location specified in the Source parameter. The page would know when the list was empty when the ClearList method would return 0, indicating that there were no items left in the list.

    Below is the server script I added to my ASPX page:

    <%

    this.Response.Buffer = false;

    string strSourceUrl = this.Request[“Source”];

    string strListUrl = this.Request[“ListUrl”];

    string strListGuid = this.Request[“List”];

    if (strSourceUrl == null && (strListGuid == null strListUrl == null))

    {

    this.Response.Write(“This page requires a ‘Source’ parameter that contains the Url of the list to clear and either a ‘List’ parameter that points to a list to clear in the current site or a ‘ListUrl’ paremeter that points to the full list path to clear.”);

    this.Response.Flush();

    }

    else

    {

    this.Response.Write(“The SourceUrl is ” + strSourceUrl + “.<BR>We will redirect there after the list has been cleared.<BR>”);

    this.Response.Flush();

    SPWeb currentWeb = null;

    SPList listToClear = null;

    if (strListUrl != null && strListGuid != null)

    {

    this.Response.Write(“ListUrl is ” + strListUrl + “<BR>”);

    this.Response.Write(“List is ” + strListGuid + “<BR>”);

    this.Response.Flush();

    currentWeb = new SPSite(strListUrl).OpenWeb();

    listToClear = currentWeb.Lists[new Guid(strListGuid)];

    }

    else if (strListUrl != null)

    {

    this.Response.Write(“ListUrl is ” + strListUrl + “<BR>”);

    this.Response.Flush();

    currentWeb = new SPSite(strListUrl).OpenWeb();

    listToClear = currentWeb.GetListFromUrl(strListUrl);

    }

    else // (strListGuid != null)

    {

    this.Response.Write(“ListGuid is ” + (new Guid(strListGuid)).ToString() + “<BR>”);

    this.Response.Flush();

    currentWeb = SPContext.Current.Web;

    listToClear = currentWeb.Lists[new Guid(strListGuid)];

    }

    this.Response.Write(“Got the list ” + listToClear.Title + “<BR>”);

    this.Response.Flush();

    //clear 50 items at a time

    currentWeb.AllowUnsafeUpdates = true;

    int intRemainingItems = BlackBlade.ListActions.ClearList.ClearListAction.ClearList(listToClear, 50);

    //do some cleanup

    currentWeb.Dispose();

    currentWeb = null;

    if (intRemainingItems > 0)

    {

    this.Response.Write(“There are still ” + intRemainingItems + ” items left to delete.<BR>Redirecting to: <a href='” + this.Request.Url.ToString() + “‘>” + this.Request.Url.ToString() + “</a><BR>”);

    this.Response.Flush();

    //there are still items left to delete. redirect back to this page

    %>

    <script type=”text/javascript”>

    window.setTimeout(“window.location = window.location”, 3000);

    </script>

    <%

    }

    else

    {

    this.Response.Write(“There are no items left to delete.<BR>Redirecting to: <a href='” + strSourceUrl + “‘ >” + strSourceUrl + “</a><BR>”);

    this.Response.Flush();

    //there are no items left to delete. go back to the source.

    %>

    <script type=”text/javascript”>

    window.setTimeout(“window.location = ‘<%=strSourceUrl %>'”, 3000);

    </script>

    <%

    }

    }

    %>

    There is one point that some might find odd: I chose to do my redirects with client-side JavaScript rather than a server-side Response.Redirect. This is because I wanted to be able to see progress intermediate progress update from the various batch item deletions. I admit this is not the most elegant approach, but it was quick and easy. I am toying around with the idea of adding some AJAX code here so I have a nicer progress indicator. I guess it will depend on my laziness.

    The List Actions Menu Item

    Next I created a CustomAction feature to add a menu item to the list Actions menu. The menu item invokes the ASPX page and passes in the Source and List parameters to the ASPX pages created earlier. The feature creation was fairly straight forward for the most part. The only snag I ran into is that I could not find a way to pass in the current Url as part of the UrlAction element. I resorted to using a JavaScript string for the UrlAction. Here’s what the feature Elements.xml looks like:

    <?xml version=”1.0″ encoding=”utf-8″ ?>

    <Elements xmlns=”http://schemas.microsoft.com/sharepoint/”>

    <CustomAction Id=”BBS.ListActions.ClearList”

    Location=”Microsoft.SharePoint.StandardMenu”

    GroupId=”ActionsMenu”

    ImageUrl=”/_layouts/images/actionssettings.gif”

    Sequence=”1000″

    Title=”Clear List Items”>

    <UrlAction Url=”Javascript:window.location = ‘{SiteUrl}/_layouts/custom/listactions/clearlist.aspx?Source=’ + window.location + ‘&amp;List={ListId}’;” />

    </CustomAction>

    </Elements>

    The {SiteUrl} portion of the Url is replaced by the SharePoint runtime with the current site Url. The {ListId} portion of the Url is replaced with the list guid at runtime.

    The Feature.xml file is unremarkable, but I’ve listed it here for the sake of completeness:

    <?xml version=”1.0″ encoding=”utf-8″ ?>

    <Feature xmlns=”http://schemas.microsoft.com/sharepoint/”

    Id=”641B0032-477D-4d1b-BBE4-85312138B420″

    Title=”Clear List Action”

    Description=”Clears all of the list items in this list”

    Scope=”Web”

    Hidden=”FALSE”>

    <ElementManifests>

    <ElementManifest Location=”Elements.xml” />

    </ElementManifests>

    </Feature>

    The Packaging

    So far, I’d taken care of requirements 1 through 3. Now I had to take care of my final requirement by creating a deployment package for my new clear list capability. Fortunately, SharePoint V3 has a new deployment mechanism called solutions, which is an extension of the web part package deployments available in SharePoint v2.

    First, I created a Manifest.xml file. The manifest.xml file tells the stsadm utility what files are part of the solution package, what type each file is, and where to place each file on the SharePoint server when the solution is deployed. I needed the code assembly to be deployed to the web application’s bin folder, and I needed a SafeControl entry created in the web application’s web.config file. The CustomAction featue.xml and elements.xml file needs to go into the ClearListItems sub folder in the Features folder in the 12 hive. Finally, I needed the ClearList.aspx file to be placed into the TEMPLATELayoutsCustomListActions folder in the 12hive. Notice that this folder maps to the Url attribute of the UrlAction element from the elements.xml file created for the CustomAction feature definition. The manifest.xml that placed the files into the correct locations is listed below:

    <Solution xmlns=”http://schemas.microsoft.com/sharepoint/”

    SolutionId=”C9F7C3AA-9B03-4a89-BBB9-D59D37D3B21B”>

    <FeatureManifests>

    <FeatureManifest Location=”ClearListItemsFeature.xml”/>

    </FeatureManifests>

    <Assemblies>

    <Assembly DeploymentTarget=”WebApplication”

    Location=”BlackBlade.ListActions.ClearList.dll”>

    <SafeControls>

    <SafeControl Assembly=”BlackBlade.ListActions.ClearList”

    Namespace=”BlackBlade.ListActions.ClearList”

    TypeName=”*” Safe =”True”/>

    </SafeControls>

    </Assembly>

    </Assemblies>

    <RootFiles>

    <RootFile Location=”TEMPLATELayoutsCustomListActionsClearList.aspx”/>

    </RootFiles>

    </Solution>

    Next, I needed to create a diamond definition file (DDF) to package all of my project files into a solution package that SharePoint can use. The WSP.DDF file is used by the MakeCab utility to translate the locations of the solution files in the VS.Net project to the locations listed in the Manifest.xml file. Here is the DDF file I used:

    ;Include the following files in the CAB Root

    manifest.xml

    ;Add the assembly

    %BuildPath%BlackBlade.ListActions.ClearList.dll

    ;Create a new folder and include the following files

    .Set DestinationDir=”ClearListItems”

    FeaturesClearListItemsFeature.xml

    FeaturesClearListItemsElements.xml

    ;Create another folder and include the following files

    .Set DestinationDir=”TEMPLATELayoutsCustomListActions”

    PagesClearList.aspx

    I won’t go into all of the syntax of DDF files here. There are lots of options for exactly tailoring the resulting CAB file and making it look just the way you need it to.

    Finally, I added the a post build event to the Visual Studio project file, calling MakeCab to generate the WSP file:

    cd “$(ProjectDir)”

    MakeCab /f WSP.ddf /D DiskDirectory1=”$(TargetDir)” /D CabinetNameTemplate=”$(ProjectName).wsp” /D BuildPath=”$(OutDir)”

    The BuildPath variable is passes into the DDF file by the command line in order to get the correct version of the assembly based on the project configuration, Debug or Release. The DiskDirectory1 variable specifies the folder location where the CAB file will be created. I wanted my file to be created in the same directory that Visual Studio was building the other project files. The CabinetNameTemplate variable specifies the filename for the resulting CAB file. I wanted the file named the same as the Visual Studio project name with a .WSP extension. My project was named BlackBlade.ListActions.ClearList so the resulting CAB file was named BlackBlade.ListActions.ClearList.WSP.

    The Deployment

    Deploying the solution package was fairly simple. I ran the stsadm utility with the addsolution option to upload the WSP to the central administration database. Then I used SharePoint 3.0 central administration to deploy the WSP solution to my SharePoint farm. Once deployed, I activated the feature on the SharePoint sites from the Site Features option in the Site Settings page.