SharePoint

Add a Recycle Bin to Windows SharePoint Services for Easy Document Recovery

Maxim V. Karpov and Eric Schoonover

This article discusses:

  • WSS object model extensibility
  • Building a custom workflow to support recycle bin functionality
  • Writing custom event handler classes
  • Web Part Page customization
This article uses the following technologies:
Office, C#

Code download available at:RecycleBinforWSS.exe(145 KB)

Contents

WSS Events
The Solution
Handling Document Library Events
Configuration
Creating the Sinks
Customization of the Web Part Pages
Conclusion

Windows® SharePoint® Services (WSS), which is part of Microsoft® SharePoint Products and Services, provides a long list of features that help improve both collaboration and workflow while protecting documents and intellectual property. While WSS features continue to improve with each release, one feature that's conspicuously missing is an easy way to back up and restore deleted files from document libraries.

In this article, we'll take advantage of the extensibility of WSS and both its server-side and client-side object models to build a restore feature that works like the Recycle Bin in Windows Explorer. We'll examine WSS document library server-side events and will develop a .NET event handler class for asynchronous server-side processing of those events. In the end, you'll have a working recycle bin for document libraries. Additionally, the lessons learned will help you write your own custom workflow applications based on the WSS document library event model.

WSS Events

WSS provides object models that allow it to be controlled and extended programmatically. At the core of the WSS server-side object model is the Microsoft.SharePoint.dll assembly, installed with the product and accessible to any code running on the server, regardless of whether that code is executing in a WSS Web context. The core of the client-side object model is found in the OWS.JS JavaScript file. This file is referenced from most pages rendered by WSS and contains the logic necessary to handle various events on the client, such as handling the selection of items from a context menu in the browser.

Figure 1 shows a high-level view of how a client interacts with document libraries in WSS. The main view of a document library is exposed from WSS via the AllItems.aspx endpoint that is located in a document library's directory. All incoming HTTP requests for AllItems.aspx handled on the server by IIS are processed through the SharePoint ISAPI filter, STSFLTR.DLL. This filter checks the Windows registry for exclusion and inclusion paths (configured by the SharePoint admin) to determine if the call should be processed by WSS.

Figure 1 WSS Client-Side Architecture

Figure 1** WSS Client-Side Architecture **

If the request is to be handled by WSS, the WSS Web Part Page handler, SharePointHandler, gets the requested content from the content database and sends it to the browser. SharePointHandler resides in the Microsoft.SharePoint.ApplicationRuntime namespace in the Microsoft.SharePoint.dll assembly and is used as the HTTP Handler for .aspx pages rendered through SharePoint. For more information, see the article by Ted Pattison and Jason Masterman in the August 2004 issue of MSDN®Magazine, available at Use Windows SharePoint Services as a Platform for Building Collaborative Apps, Part 2.

Dynamic HTML (DHTML) provides client-side event handling. For example, hovering over a document in a document library and clicking the arrow that appears causes a custom context menu to be displayed in the browser, and this menu provides for common document operations such as delete, check in, and check out. When the user selects the delete menu item from the context menu, a JavaScript event fires, causing an HTTP request to be made to the server, the result of which is that the specified file is deleted and a server-side delete event is raised.

The complete list of server-side document library events, as it exists in the current version of WSS, are as follows: cancel check out, check in, check out, copy, delete, insert, move or rename, and update. If you want to handle server-side events, you must develop a custom event handler class, called an event sink, that can then be registered to handle the events for a particular document library. This configuration can be performed using properties of the document library exposed through the Web administration interface, or it can be done programmatically using the server-side object model exposed by SharePoint.

The WSS document library event pattern is similar to the Observer (Producer-Consumer) pattern. However, while multiple WSS document libraries can be registered as event producers for the same event sink, only one event sink per document library can be notified that events were raised, as shown in Figure 2. This many-to-one relationship means that all event handling logic for a single document library must be encapsulated in a single event sink. Alternatively, if a chain or a set of event sinks is required, a custom event sink could be written to receive the document library's events and then delegate them to other handlers.

Figure 2 Producer-Consumer Pattern

Figure 2** Producer-Consumer Pattern **

In order to establish the relationship of producer to consumer, you must set the EventSinkAssembly and EventSinkClass properties of the target document library to point to the event sink. Event sinks are .NET classes that implement the IListEventSink interface and its only method, OnEvent. This method has one SPListEvent input parameter, which contains information about the event that occurred, such as the type of the event (available from the Type property, an SPListEventType), the location of the file before the event (available from UrlBefore), and the location of the file after the event (available from UrlAfter). The assembly containing the event handler class should be strongly named and installed into the Global Assembly Cache (GAC).

Before any document libraries can raise events on the server, WSS must be configured to enable events. The default configuration of a WSS virtual server does not allow events to be raised for document libraries, so you have to explicitly enable this feature before your custom event sink objects will be notified of events. You can do that through the administration Web site, by using the SharePoint command-line administration tool STSADM.EXE, or by using the WSS server-side object model. Enabling event handlers programmatically is simple using code like that shown here:

SPGlobalAdmin globalAdmin = new SPGlobalAdmin(); System.Uri uri = new System.Uri("https://localhost:8080"); SPVirtualServer vServer = globalAdmin.OpenVirtualServer(uri); SPVirtualServerConfig vConfig = vServer.Config; vConfig.EventHandlersEnabled = true; vConfig.Properties.Update(); globalAdmin.Close();

Given this description, you might think of an easy solution for implementing recycle bin functionality. You would simply create a document library and delegate it as the recycle bin. When delete events were raised on other libraries, you would move the deleted documents to the recycle bin library rather than deleting the document. From there the user could restore it at a later time, resulting in the document moving back to the original library, or could empty the bin permanently removing all documents that had previously been deleted. The following code snippet is skeleton code for this proposed solution, where the MoveToRecycleBin method would simply copy the document at EventInfo.UrlBefore to the recycle bin library:

public class CustomEventHandler : IListEventSink { public void OnEvent(Microsoft.SharePoint.SPListEvent e) { switch (EventInfo.Type) { case SPListEventType.Delete: MoveToRecycleBin(); break; } } }

Being able to move the deleted document to a recycle bin library by simply handling the Delete event type would be an ideal solution, but unfortunately there's a problem with this approach: events are processed asynchronously. As a result, a registered event sink will only be notified about the document deletion after that document has already been deleted from the SQL Server™ backend database. As a result, the event sink can't simply copy the document to the recycle bin library because the deleted document no longer exists.

The Solution

The solution we present is a simple one that still makes use of WSS document library events. Instead of attempting to copy the document from the document library when the document is deleted, we create a mirror of the document library that will hold copies of all the files, as shown in Figure 3. This mirror library is a normal document library with its own custom event sink, but more on that in a moment.

Figure 3 Maintaining a Mirror Document Library

Figure 3** Maintaining a Mirror Document Library **

When files are inserted or updated in the main document library, a custom event handler object will copy the file into the mirror document library in order to keep the mirror current. When a user deletes a file, the event handler will move the copy of the file from the mirror document library into the recycle bin document library, solving the problems caused by the asynchronous processing of events. This will, of course, incur some database overhead because there will now be two copies of the same document (one in the main document library and one in its mirror), so this implementation may not be a good solution for large databases. This is why we provide the ability to filter the documents which are mirrored, based on the document's type and size. This filter can be set using the event sink's SinkData property, allowing users to back up files based on these filter criteria.

Keep in mind, however, that there's more to this implementation than just the handling of events on the server. We also need to customize the recycle bin document library in order to override the default document context menu displayed on the client. This context menu will sport custom menu items that support our custom operations, such as restore.

Because there is no restore event in WSS, our context menu reuses the OWSSVR.DLL Save method. When the restore option is selected, an event is triggered on the server. The event handler class for the recycle bin document library will be notified of the event and will copy the file to the main document library, thus restoring the file. Of course, with the document now back in the main library, the mirror library also needs a copy. One might expect this to be handled automatically by the event handler that copies from the document library to its mirror, but actions performed by event handlers don't result in other events being raised. So, when you copy the file to the main document library, its insert event handler is not triggered. As a result, in addition to copying from the recycle bin library to the main document library, we also programmatically copy the file to the document library's mirror.

Ultimately, the solution consists of two parts: a pair of event handler classes to enable a workflow for moving files around between document libraries based on events, and a customized user interface for a special document library, which will serve as a recycle bin, overriding the default document library context menu displayed for the files in the library.

Handling Document Library Events

We're handling events for two different categories of document libraries: the main library for which we want to add restore capabilities, and the recycle bin library which will catch deleted documents. As such, we created two different event sink classes for handling events.

In April 2004, the Microsoft SharePoint team released the Document Library Event Handler Toolkit to assist in event handler development. The sample comes with a BaseEventSink class, which contains most of the boilerplate code commonly associated with writing event sinks. In particular, it encapsulates the impersonation code that allows for event handlers to access secure resources under a specific user account. The base class provides the HandleIdentity method, which uses the Windows API in conjunction with the WindowsIdentity class to create a new temporary identity for the event handler. This is used to get around the fact that, by default, event handlers execute under IIS 6.0 application pools, which by default run under the unprivileged account of NT AUTHORITY\NetworkService. Since events are processed asynchronously, the client can't be impersonated in the event handler, resulting in the default account being used.

The Document Library Event Handler Toolkit also contains sample code for event handlers that perform a variety of tasks, including the creation of a mirrored document library, the logging of events to a list, and the sending of events to a custom monitoring application using .NET Remoting. We've reused the BaseEventSink class as a base class for our custom event handlers.

Configuration

In order for this workflow to function correctly, we've used the SinkData text property for event sinks (configured along with EventSinkClass and EventSinkAssembly), which has a limit of 255 characters, to provide configuration data. For each document library configured to support the restoration of files, this data includes the name of the mirror library, the name of the recycle bin library, identity information to allow for impersonation, and filter information for purposes discussed earlier:

<Data> <Mirror>Backup</Mirror> <Recycle>Recycle Bin</Recycle> <Domain>TRAINING</Domain> <User>SomeUser</User> <Pass>pT19n3$ql</Pass> <Type>*</Type> <Size>*</Size> </Data>

The SinkData property can be configured through the Advanced Settings of the document library administration page. Note that in our sample, the credentials are stored as clear text. When writing production code, we recommend you use the .NET cryptography classes to encrypt the password.

Instead of parsing the configuration data, we chose to use a custom EventSinkData class that supports the serialization and deserialization of an XML string into an instance of an object. This lets us easily access configuration data as properties on an object rather than having to manually parse the information from the configuration data. For the XML string to be serialized into the EventSinkData object type, we used this code:

public static EventSinkData GetInstance(string text) { using(MemoryStream mstr = new MemoryStream( Encoding.UTF8.GetBytes(text)) { XmlSerializer ser = new XmlSerializer(typeof(EventSinkData)); return (EventSinkData)ser.Deserialize(mstr); } }

This configuration data is then exposed through a new property we added to BaseEventSink called Configuration, making the configuration information accessible to our custom derived sinks.

Creating the Sinks

The BaseEventSink class declares one abstract HandleEvent method to be overridden by our derived classes, DocLibEventSink and RecycleBinEventSink, allowing us to capture the events and create the proper workflow.

Let's begin with the DocLibEventSink class. As discussed, this sink creates copies of the documents inside of the mirror document library and moves the copy of a deleted file from the mirror library to the recycle bin. These actions need to be performed under a security context that has permissions to do so. That is why when deriving from the BaseEventSink class, the HandlerIdentity method should be overridden. It is important to provide the HandlerIdentity method with credentials that have the proper level of access to all the WSS resources that will be affected by the event handler. This includes the main document library, the mirror document library, and the recycle bin document library. The derived DocLibEventSink class will have the following code for the method:

protected override WindowsIdentity HandlerIdentity { get { if (m_Identity == null) { m_Identity = CreateIdentity(Configuration.UserName, Configuration.Domain, Configuration.Password); } return m_Identity; } }

This method calls into the base class's Configuration property, which we added earlier, and returns credentials that are impersonated while processing events. The plain text credentials are turned into a WindowsIdentity using the base class's CreateIdentity method, which in turn uses P/Invoke to access the Win32® LogonUser function. With impersonation in place, you need to override the HandleEvent method and provide logic that will handle each of the specific events, as shown in Figure 4.

Figure 4 Overriding HandleEvent

protected override void HandleEvent() { switch (EventInfo.Type) { case SPListEventType.Delete: DeleteHandler(); break; case SPListEventType.Move: if (Include()) MoveHandler(); break; case SPListEventType.Update: case SPListEventType.Insert: case SPListEventType.Copy: if (Include()) UpdateInsertCopyHandler(); break; } }

Here you can access the EventInfo property of the base class, which contains an instance of SPListEvent representing the event that was raised. This enables the document library to determine what type of event occurred. If files are being updated in the document library, the event handler calls the UpdateInsertCopyHandler method to ensure that a copy of the file is made to mirror the document library. In the case where a user deletes a file from the document library that has a recycle bin associated with it, the DeleteHandler method is executed to copy the file from the mirror library to the recycle bin, as shown here:

private void DeleteHandler() { SPFile delFile = EventWeb.GetFile(String.Concat( Configuration.MirrorLib, "/", EventInfo.UrlBefore)); string newFileLoc = String.Concat(Configuration.RecycleLib, delFile.Url.Remove(0, Configuration.MirrorLib.Length)); EnsureParentFolder(EventWeb, newFileLoc); delFile.MoveTo(newFileLoc, true); }

To access the file that has been stored in the mirror, you can call the EventWeb base class property. This returns an SPWeb object corresponding to the location of the event. The Web's GetFile method can then be used with the EventInfo.UrlBefore property to access the SPFile object for the mirrored document.

To support one recycle bin document library for many document libraries, we use a folder structure inside the mirror and recycle bin that correspond to the names of the document libraries from which the files were deleted. The EnsureParentFolder method of the base class creates and ensures that the folder structure for the mirror and recycle bin libraries are intact.

The RecycleBinEventSink class is designed to provide the workflow for the restore functionality. Since there is no restore event, we reuse client-side Insert and Save events (Save corresponds to Update on the server) in order to trigger the copy of the file from the recycle bin document library into the document library of its origin. By overriding the HandleEvent method in the sink, we specify custom logic for the Insert and Update events by calling into the UpdateInsertHandler method. This retrieves files from the recycle bin document library using the EventWeb property of the base class, and again we use the Web's GetFile method to access the file that will be moved to the main document library and copied to the mirror. As mentioned earlier, actions initiated by an event handler do not cause events to be raised, so an event is not raised when the file is moved to the main document library through the event handler. This means you have to manually ensure that the file gets copied into the mirror library. If you want to change your configuration to use different mirror libraries, you first need to retrieve configuration information from the main document library, as shown in the following:

string strCfg = EventWeb.Lists[doclibName].EventSinkData; EventSinkData cfg = EventSinkData.GetInstance(strCfg);

To recap, we used the BaseEventSink class from the Document Library Event Toolkit and added features to support XML serialization for configuration data. We then created two classes, DocLibEventSink and RecycleBinEventSink, which derive from the BaseEventSink class and which encapsulate the necessary server-side logic to implement a recycle bin. With the server-side logic in place, we now need to create and customize our document library views to support a restore menu item for the recycle bin.

Customization of the Web Part Pages

The document library's Web page user interface is based on the WSS Web Part Page framework, which extends the ASP.NET control model. The idea behind the Web Part Page framework is to allow the Web designer to customize the contents of the workspace and to allow the end user to personalize the information using Web Parts. Document libraries come with five built-in views, as shown in Figure 5.

Figure 5 Document Library Web Part Pages (Views)

File Name Description
AllItems.aspx Default view showing all the items in the document library
DispForm.aspx Page showing the document properties
EditForm.aspx Page showing the form for modifying document properties
Upload.aspx Page holding the upload form for uploading document to the library
WebFldr.aspx Page holding the Explorer View which is an ActiveX component for manipulating the files

The default view, AllItems.aspx, shows contents and files with a column and row structure. When a new document library is created, the physical ASPX files (available at \60\TEMPLATE\1033\ STS\LISTS\DOCLIB\), are used in conjunction with the SCHEMA.XML template file. A Collaborative Application Markup Language (CAML) definition is used for the fields and scripts, producing five entries inside of the WSS content database. For example, when AllItems.aspx is invoked, the SharePointHandler endpoint retrieves data for the page's ListViewWebPart Web Part and populates its ListViewXml property with values from the database. The output is sent to the Web browser.

There are only two options for modifying design-time information for the views: using a Web browser and the administrative view provided by WSS or using Microsoft FrontPage® 2003. WSS stores all its contents inside its content database, and that information is retrieved at run time by the SharePointHandler. To retrieve design-time values for the Web Part Page, FrontPage uses a combination of FrontPage Server Extensions, WSS Web services, and remote procedure calls (RPC) to retrieve the view and save it back to the database (see Figure 6).

Figure 6 FrontPage Design-Time View

Figure 6** FrontPage Design-Time View **

This process only needs to be performed for one recycle bin library, since the library can be saved as a template and reused on other servers. Start by launching FrontPage 2003, creating a new document library, and naming it "Recycle Bin." Next, open the AllItems.aspx page for the library, modify the page in FrontPage 2003, and select the List View WebPart, which is located in the Main Web Part Zone on the page. Disable the toolbar on the document library. From the List View Options, select Style to open the View Styles dialog box. Next, select the Options tab and uncheck the "Show toolbar with options" box and click OK. Finally, save the page and exit FrontPage 2003. This will disable standard document library options such as New Document, Upload Document, and others. Since this document library is going to serve as a recycle bin, it is not necessary to have those options enabled.

Next, switch to code view and paste the JavaScript code shown in Figure 7 between the head tags for the Web Part Page. This script overrides the default menu items by specifying the custom menus that contain Restore and Delete items. OWS.JS contains a client-side JavaScript object model, and through extensibility points it is possible to override menus without modifying the original file. One such point is the following code from OWS.js:

function AddDocLibMenuItems(m, ctx) { if (typeof(Custom_AddDocLibMenuItems) != "undefined") { if (Custom_AddDocLibMenuItems(m, ctx)) return; } ... // build regular menu }

Figure 7 Overriding Default Menu Items

function Custom_AddDocLibMenuItems(m, ctx) { var RootFolder = GetRootFolder(ctx); setupMenuContext(ctx); if (currentItemFileUrl == null) currentItemFileUrl = itemTable.ServerUrl; if (currentItemFSObjType == null) currentItemFSObjType = itemTable.FSObjType; var currentItemEscapedFileUrl = escapeProperly(unescapeProperly(currentItemFileUrl)); // document library might contain folders; thus we need to enable // default delete on the folder if (currentItemFSObjType == 1) { strDisplayText = L_DeleteDocItem_Text; strAction = "DeleteDocLibItem('" + ctx.HttpPath + "&Cmd=Delete&List=" + ctx.listName + "&ID=" + currentItemID + "&owsfileref=" + currentItemEscapedFileUrl + "&NextUsing=" + GetSource() + "')"; strImagePath = ctx.imagesPath + "delitem.gif"; CAMOpt(m, strDisplayText, strAction, strImagePath); } else // build custom menus for the files { // build restore menu item and wire it to trigger Update var L_Restore_Text = "Restore File"; strDisplayText = L_Restore_Text; strAction = "SubmitFormPost('" + ctx.HttpPath + "&Cmd=Save&List=" + ctx.listName + "&ID=" + currentItemID + "&owsfileref=" + currentItemEscapedFileUrl + "&NextUsing=" + GetSource() + "')"; // reused approval image, but custom image can be // associated with the menu item strImagePath = ctx.imagesPath + "APPROVE.GIF"; CAMOpt(m, strDisplayText, strAction, strImagePath); // build regular delete menu item to trigger a complete delete // of the file strDisplayText = L_DeleteDocItem_Text; strAction = "DeleteDocLibItem('" + ctx.HttpPath + "&Cmd=Delete&List=" + ctx.listName + "&ID=" + currentItemID + "&owsfileref=" + currentItemEscapedFileUrl + "&NextUsing=" + GetSource() + "')"; strImagePath = ctx.imagesPath + "delitem.gif"; CAMOpt(m, strDisplayText, strAction, strImagePath); } return true; }

The JavaScript code in Figure 7 contains a function called Custom_AddDocLibMenuItems, which integrates with this code from OWS.js and contains logic for adding the Delete menu item for a Folder type and the Restore and Delete items for file types. We mentioned before that there is no built-in restore event, so we're reusing the Save functionality of the OWSSRV.DLL.

The GetSource function results in a redirect to the specified page after the event has been sent to the server. Because there is no event for the Restore, we have to use the Save command to trigger the restoration process on the server. Unfortunately, there is a little bug with this JavaScript code that we could not overcome. Sometimes, when the file size is large, it takes time to move the file from the recycle bin to the document library of its origin. The GetSource function does not wait until the server processes are complete and the document library view is refreshed. The file therefore still appears in the view even though the command has been submitted to OWSSRV.DLL. If you refresh the page manually after the operation has completed, the file will most likely disappear from the document library.

Because the mirror document library contains backup copies of the files, it is a good idea to hide it from the users to prevent tampering. Using FrontPage 2003, open the site that contains the mirror, right-click the mirror document library folder and select Properties. Select the Settings tab and check the Hide from browsers checkbox. This will hide the document library in the site's list of lists (users will still be able to access the document library by using the UNC path in Windows Explorer).

At this point, the infrastructure for the client and the server side is set, so enable the document libraries for which you want to have recycle bin functionality by specifying custom event handlers in the Advanced properties.

Conclusion

Because files deleted from WSS are not recoverable without lots of pain and administrator intervention, recycle bin functionality on the WSS document libraries is a welcome customization. This safety net is important to both portal users and administrators since trying to reload a portal backup for only a few documents is tedious and often results in the loss of more data than it saves.

Maxim V. Karpov is an independent consultant and software architect. He is completely sold on the idea that design patterns provide developers with a universal language for communication and a platform for developing superior application architectures. He can be reached at https://ipattern.com.

Eric Schoonover is a Senior Application Developer for Avanade Inc. Based in Seattle, he travels throughout the country designing portals for the insurance, financial, defense, media, and software industries. Eric can be reached at ericscho@avanade.com.