Friday 22 July 2011

SharePoint: Drag and Drop File Uploads with Ajax

I recently came across Ajax upload; a bit of javascript that allows users to drag and drop files from their computer into an area on a web page, and those files to be uploaded to the server. It looks quite nifty and I thought it would be a nice addition for my SharePoint 2010 site.

How does the upload script work?

The first step to take was to figure out how Ajax upload works. After checking the site and examples, comments, etc, it turned out that for each file dragged/chosen for the uploader, a separate POST request is sent to a location you specify containing the file to be uploaded. The script then awaits a JSON success message to come back.

A little gotcha to this was that the location of the uploaded file is stored differently in the Request object depending on the browser that is used. For IE, we need to check the Request.Files collection and for other all other browsers we check Request.InputStream. I'll come to this later.

Aspx Page Setup

So to get the uploader up and running, I created a FileUploader.aspx page (not very original I know) which will show an upload form to the user as well as accept post requests for uploaded files i.e. it will post uploads to itself.  For my own purpose, the page expects a queryString param of "destination" to be present, this contains the destination of the SharePoint folder the files should be uploaded to.

I've added a div with id="fileuploader" in this page which will be the drag and drop area ajax upload will work with.  I then added the javascript below to initialise ajax uploader. "this.FolderLocation" is a a server side page property set on Page Load, retrieved from the query string as we will see later. The styles used by ajax uploader can easily be changed via the css file associated with it and the classes the uploader uses can be changed within the setup javascript. I added an extra parameter for ajax uploader to send on each post which is "destination", this is the complete link to the file location the uploaded file should be placed. At the moment, the file will be automatically overwritten if it already exists, but you could add an optional parameter at this point to be passed with each POST request to specify true or false for this action.

function SetupUploader(destination)
{
    var uploader = new qq.FileUploader({
        element: document.getElementById('fileuploader'),
        action: '/_layouts/myWebApp/FileUpload.aspx',
        onSubmit: function (id, fileName) {
            uploader.setParams({
                destination: destination + fileName
            })
        },
        template: '<div class="qq-uploader">' +
            '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
            '<div class="qq-upload-button">Upload a file</div>' +
            '<ul class="qq-upload-list"></ul>' +
            '</div>',
        classes: {
            // used to get elements from templates
            button: 'qq-upload-button',
            drop: 'qq-upload-drop-area',
            dropActive: 'qq-upload-drop-area-active',
            list: 'qq-upload-list',

            file: 'qq-upload-file',
            spinner: 'qq-upload-spinner',
            size: 'qq-upload-size',
            cancel: 'qq-upload-cancel',

            // added to list item when upload completes
            // used in css to hide progress spinner
            success: 'qq-upload-success',
            fail: 'qq-upload-fail'
        },
        onComplete: function (id, fileName, responseJSON) {
        }
    });
}

$(document).ready(function () {
    SetupUploader('<%= this.FolderLocation %>/');
});

Page Html
For completeness, the layout code below was used (minus register tags), you can see the "fileuploader" empty div present ready for JQuery to be applied to it.
<asp:Content ID="AdditionalHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead"
    runat="server">
    <SharePoint:ScriptLink ID="ScriptLink2" runat="server" Name="Styles/myWebApp/js/jquery-1.6.1.min.js" />
    <SharePoint:ScriptLink ID="ScriptLink1" runat="server" Name="Styles/myWebApp/js/custom/fileUpload.js" />
    <SharePoint:ScriptLink ID="ScriptLink5" runat="server" Name="Styles/myWebApp/js/fileuploader.js" />
    <link type="text/css" rel="stylesheet" href="/_layouts/1033/Styles/myWebApp/fileuploader.css"title="normal" media="screen" />
    <link type="text/css" rel="stylesheet" href="/_layouts/1033/Styles/myWebApp/styles.css" title="normal" media="screen" />
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <script type="text/javascript">
        $(document).ready(function () {
            SetupUploader('<%= this.FolderLocation %>/');
        });
    </script>
    <table border="0" cellspacing="0" cellpadding="0" width="100%">
        <tr>
            <td>
                <wssuc:inputformsection runat="server" title="Upload files" description="Browse or drag and drop the files you would like to upload. These will overwrite any existing versions.">
                    <template_inputformcontrols>
                        <table width="600px">
                            <tr>
                                <td valign="top" style="width: 50px">
                                    Destination
                                </td>
                                <td>
                                    <%= this.FolderLocation %>
                                </td>
                            </tr>
                            <tr>
                                <td valign="top">
                                    Browse or drag and drop files:
                                </td>
                                <td>
                                    <div id="fileuploader">          
                                 </div>
                                </td>
                            </tr>
                        </table>
                    </template_inputformcontrols>
                </wssuc:inputformsection>
                <div class="C_DASHBOARD_DIALOG_DIALOG_BUTTONS">
                    <div class="C_DASHBOARD_DIALOG_DIALOG_LINK">Close</div>
                </div>
            </td>
        </tr>
    </table>
</asp:Content>
Server Side setup

We have setup the client side, now its time to see how the back end works.  The page below has been deployed as a TemplateFile to the Template\Layouts\MyWebApp directory.  The page load event will act in two ways depending on the request type. A GET request will grab the folder location the ajax uploader should include in its post request when adding documents, the POST requests will actually add the document to SharePoint.


public partial class FileUpload : LayoutsPageBase
    {
        // our folderLocation page property used in the client for ajax upload
        public string FolderLocation
        {
            get
            {
                return ViewState["FolderLocation"] != null ? ViewState["FolderLocation"].ToString() : "";
            }
            set
            {
                ViewState["FolderLocation"] = value;
            }
        }

        protected override void OnLoad(EventArgs e)
        {
            // If no POST has occurred, its just the initial load of the page, so set the page property from query string
            if (Request.RequestType != "POST")
            {
                FolderLocation = Request.QueryString["folderlocation"];
                return;
            }

            // the post request should have a destination param, return an error if not
            if (Request.QueryString["destination"] == null)
            {
                WriteResponse("{\"success\": false, \"error\": \"wrong or missing parameters\"}");
                return;
            }
            AddDocument();
        }

        private void AddDocument()
        {
            // Get the stream object by checking Files collection or InputStream
            Stream fStream;
            if (Request.Files.Count > 0)
                fStream = Request.Files[0].InputStream; //IE does this
            else
                fStream = Request.InputStream;

            // This is actually in my service layer, moved here for completeness
            using (SPSite site = new SPSite(Request.QueryString["destination"]))
            using (SPWeb web = site.OpenWeb())
            {
                web.AllowUnsafeUpdates = true;
                web.Files.Add(Request.QueryString["destination"], fStream, new SPFileCollectionAddParameters() { Overwrite = true });
                web.AllowUnsafeUpdates = false;
            }
            // important to return true success for ajax upoad to act correctly
            // can also wrap above in try catch and return error in catch
            WriteResponse("{\"success\": true}");
        }

        private void WriteResponse(string output)
        {
            Response.Clear();
            Response.ContentType = "text/html";
            Response.Write(output);
            Response.End();
        }
    }
See if it works

So we have our client side script in place, our back end code deployed as well as the page template itself residing in the layouts folder, so we should be able to upload some documents via drag and drop. I've pasted in step by step demo in pictures below:

Step 1: Dragging files, the ajax uploader changes to red area to drag onto

Step 2: Upload in progress

Step 3: Upload complete, Firebug shows the 2 seperate POST requests

Step 4: Documents shown in the library (via custom user control using datatables)
Summary

I hope this has shown how we can easily add a bit of value for the end user by enabling drag and drop file capability for SharePoint without too much fuss. There are quite a few options for ajax uploader that can be utilised, such as "allowedExtensions" to limit file types, or "sizeLimit" for a size cap on files, just check the documentation for more information.

Monday 11 July 2011

SharePoint 2010 Excel REST API and JQuery

SharePoint 2010 has a pretty cool feature that is the Excel REST API. There are plenty of articles explaining what links to use and how to view the data as html in our sites, but how can we as developers use this data in a a less static way on our websites and really improve the user experience?

Enter JQuery, and specifically the excellent DataTables plugin. JQuery DataTables provides a way of making any HTML table searchable/filterable/sortable/pageable plus other optional plugins.

Step 1: Upload Excel document to a document library
Ok, so I'm going to upload an Excel document that contains the staff contact list.  I've created columns: Name, Mobile, Phone No, Location and set a range around all the rows of data so that Excel web services can pick up this range.  You could also insert a table into Excel rather than using ranges, whatever suits.

Setting up the contact list in Excel
I saved this and uploaded to a Document library in my Team Site and tested that Excel REST picks up the document by browsing to it:

http://mywebapp/_vti_bin/ExcelRest.aspx/Shared%20Documents/ContactList.xlsx/model

I can then click Ranges, copy the link to my range name, paste it in the address bar and change the atom param to "html" to view the Html Table Excel Rest provides:
Excel REST Html Table of my contact list
Step 2: Use JQuery to edit the table for datatables use
If you look at the source of the page above, you would see a HTML table with a few things that need correcting for Datatables to be able to use it.  It has no THead elements for the first row, it also adds a blank column and row for some reason.  Also, if colours and styling have been applied in Excel, all these inline styles are encoded into the table:

Excel REST Html Table
So in our web page, I have added a blank div element, JQuery will fill up this element on document ready with our datatable contact list via an AJAX request to the REST link. On receipt of the HTML, we will get rid of all the styling elements, the extra columns and rows and ensure the first row is within a THEAD section with TH elements.  Then we will apply DataTables, as well as reporting an error if the link to the document has moved and the AJAX query fails:

$(document).ready(function () {
    jQuery.ajax({
        type: "GET",
        url: 'http://mywebapp/_vti_bin/ExcelRest.aspx/Shared%20Documents/contactlist.xlsx/model/Ranges%28%27Contact_List%27%29?$format=html',
        data: "",
        contentType: "text/html; charset=utf-8",
        dataType: "html",
        success: function (data) {
            // fill our empty div with the html table and assign it an id
            $('#section').html(data.replace("<table ", "<table id='contacts' "));
            
            // get our newly created table within the div
            var table = $('#contacts');

            //get rid of styling from all elements
            $(table).removeAttr("style").removeAttr("class").removeAttr("cellpadding").removeAttr("cellspacing").addClass("display");
            $('tr', table).removeAttr("style").removeAttr("class");
            $('td', table).removeAttr("style").removeAttr("class");
            $('div', table).removeAttr("style").removeAttr("class");
            
            // Now remove the blank first column and row
            $('tr', table).first().remove();
            $('tr td:first-child', table).remove();

    //Create a thead element and copy the first row contents to it, making each tr a th
            $('tr:first', table).html(function (index, oldHtml) {
                return oldHtml.replace(/<td/g, '<th').replace(/<\/td/g, '</th');
            });
            $(table).wrapInner('<thead />');
            $('tr', table).first().appendTo($('thead', table));
            $('tbody', table).appendTo(table);

            // apply datatables to it
            $(table).dataTable({
                "aaSorting": [],
                "sPaginationType": "full_numbers",
                "sDom": '<i>frt<"C_TABLE_BOTTOM NIHR_TOOLBAR"lp>',
                "bAutoWidth": false
            });
        },
        error: function (xhr, msg, e) {
            $('#section').html("<span style='color: red'>Error: The section data cannot be found. Has the underlying file been deleted or moved?</span>");
        }
    });});

Step 3: See if it works
If your web page has the JQuery and the jquery.datatables scripts loaded, then you should see your table working with datatables correctly. You should be able to sort, page and search through the table too.


Contact Excel table with JQuery DataTables applied

This is a scaled down version of what I have done in my own site where I use the simplerss plugin to retrieve the various Ranges, Tables, Charts and Pivots for the user to choose from an uploaded Excel file to broaden the range of what can be used from Excel REST services. 

I hope this has provided a good example of how we can let the user use a product they are comfortable with and then being able to easily embed data from their work into a website. It adds a lot of extra value for the end site users and updates to the data are automatically applied when the user updates the Excel document.