Business Solution: Integrating a SilverLight multi-file uploader control

From Sense/Net Wiki
Jump to: navigation, search
  •  
  •  
  •  
  •  
  • 100%
  • 6.0.5
  • Enterprise
  • Community
  • Planned

Overview

This solution applies to versions prior to 6.2.1. From version 6.2.1 use the upload action as described in Upload action

Problem description: I found a fancy Silverlight uploader on the net and I want to use it instead of the built-in Flash uploader control.
Solution: We will create a Sense/Net portlet and a control to host the third-party SilverLight control, and create a handler to process incoming files.

Details

We will integrate the Silverlight File Upload by darick_c into Sense/Net, which is a great Silverlight 2 compatible component that supports multiple file uploading. You can find other great multi-file uploaders on the web, like the Silverlight Multi File Uploader by Michiel Post, they work pretty much the same way, so with some tweaks you will be able to use the process discussed below to integrate any kind of third-party uploader component into the system. Our uploader project will be built up according to the following:

  • the SilverLight application will be hosted in a control, and a portlet will be used to enable easy deployment for portal builders,
  • source code of the third-party component will not be used, only dll,
  • the uploader posts file chunks to an IHttpHandler which saves the file to a folder on the web server in file system,
  • we will put the uploaded files into the Content Repository when upload is completed,
  • the user can select the Content Type to be created when uploading files.

Steps

1. Create a new project to contain all new code

Create a new project in Visual Studio! The structure of the solution folder should look like this:

  • Sl2FileUpload
    • CoreReferences - holds Sense/Net references, copy all dll-s from the Sense/Net webfolder/bin directory here
    • References - holds third-party tool references, copy DC.SilverlightFileUpload.dll and System.Web.Silverlight.dll here
    • Root - will contain the .ascx control and .ashx handler in the same structure as they will be placed in the Content Repository
    • Sl2FileUpload - will contain all code files
      • Sl2FileUpload.csproj
    • Sl2FileUpload.sln

When the project is created, add references to it:

  • Sense/Net references:
    • SenseNet.Portal
    • SenseNet.ContentRepository
    • SenseNet.Storage
  • third-party references:
    • DC.SilverlightFileUpload.dll
    • System.Web.Silverlight.dll
  • .Net references
    • System.Web
    • Syste.Web.Extensions

2. Create the control to host the SilverLight application

Now let's create the control that will contain the SilverLight uploader control. We will create two files for the control and the code-behind:

  • Sl2FileUpload\Root\System\SystemPlugins\Controls\Sl2FileUploadControl.ascx
  • Sl2FileUpload\Sl2FileUpload\Sl2FileUploadControl.cs

Let's look at the control first:

<%@ Control Language="C#" AutoEventWireup="true" Inherits="Sl2FileUpload.Sl2FileUploadControl" EnableViewState="true" %>
<%@ Register Assembly="DC.SilverlightFileUpload" Namespace="DC.SilverlightFileUpload" TagPrefix="DC" %>
 
<asp:UpdatePanel ID="up1" runat="server">
    <ContentTemplate>
        <div>
            Upload folder path:
            <asp:Label ID="lblFolderPath" runat="server"></asp:Label>
	    <br />
            Upload as:
            <asp:DropDownList ID="ddContentTypes" runat="server"></asp:DropDownList>
	    <br />
        </div>
        <div>
            <DC:MultiFileUploadControl runat="server" ID="fileUpload" Width="700" Height="400" 
            UploadPage="/Root/System/WebRoot/Sl2FileUpload.ashx" 
            MaxConcurrentUploads="1" Filter="All Files (*.*)|*.*|Images (*.jpg;*.gif;*.jpeg;*.png;*.bmp)|*.jpg;*.gif;*.jpeg;*.png;*.bmp" />
        </div>
    </ContentTemplate>
</asp:UpdatePanel>

So as you see, we will use the class Sl2FileUpload.Sl2FileUploadControl (we will create it soon), and render the following controls:

  • a label to display the current Content Repository folder where files will be uploaded,
  • a dropdown from which the user can select Content Types,
  • the SilverLight multiple file upload control - with the UploadPage property already set to the handler, that will accept the uploaded chunks - the handler will be created later on.

Note, that we use an UpdatePanel, so that when the user selects a Content Type, the page is not reloaded. Now there are a couple of things that needs to be executed when the control is rendered:

  • the UploadPage property only contains the target handler path as defined above, but some other parameters will have to be sent to the handler so that it will know what Content Type to create and where to create it. These parameters will be sent using url query params:
    • nodeid of the target folder will be sent
    • Content Type name will be sent
    • a guid handle to identify the session, so that multiple users will be able to upload the same files to different locations simultaneously.
  • the Content Type dropdown should contain the list of Content Types, this will be populated on server side
  • when the user selects a Content Type, we should register his/her selection and modify the UploadPage property of the SilverLight uploader accordingly.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SenseNet.ContentRepository;
using System.Web.UI.WebControls;
using SenseNet.ContentRepository.Storage;
using DC.SilverlightFileUpload;
using System.Web;
using SenseNet.Search;
using SenseNet.Portal.Handlers;
using SenseNet.Portal.UI.Controls;
 
namespace Sl2FileUpload
{
    public class Sl2FileUploadControl : System.Web.UI.UserControl
    {
        // =============================================================== Public properties
        public Node ContextNode { get; set; }
 
 
        // =============================================================== Protected properties
        protected Label lblFolderPath
        {
            get
            {
                return this.FindControlRecursive("lblFolderPath") as Label;
            }
        }
        protected DropDownList ddContentTypes
        {
            get
            {
                return this.FindControlRecursive("ddContentTypes") as DropDownList;
            }
        }
        protected MultiFileUploadControl Uploader
        {
            get
            {
                return this.FindControlRecursive("fileUpload") as MultiFileUploadControl;
            }
        }
 
 
        // =============================================================== Methods
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
 
            // init UI
            lblFolderPath.Text = ContextNode.Path;
            // on postback content type list may already contain content types
            if (ddContentTypes.Items.Count == 0)
            {
                var contentTypeNames = GetFileContentTypesNames();
                ddContentTypes.Items.Add(new ListItem(UploadHelper.AUTOELEMENT, UploadHelper.AUTOELEMENT));
                foreach (var contentTypeName in contentTypeNames)
                    ddContentTypes.Items.Add(new ListItem(contentTypeName, contentTypeName));
            }
            ddContentTypes.SelectedIndexChanged += new EventHandler(ddContentTypes_SelectedIndexChanged);
            ddContentTypes.AutoPostBack = true;
 
            // init upload parameters
            // on postback uploadpage may already contain parameters
            if (!Uploader.UploadPage.Contains('?'))
            {
                Uploader.UploadPage += string.Format("?ParentId={0}&UploaderToken={1}&ContentType={2}",
                    HttpUtility.UrlEncode(ContextNode.Id.ToString()),
                    Guid.NewGuid().ToString(),
                    ddContentTypes.SelectedValue);
            }
        }
        protected void ddContentTypes_SelectedIndexChanged(object sender, EventArgs e)
        {
            // modify UploadPage property according to the newly selected Content Type
            var selectedType = ddContentTypes.SelectedValue;
            var idx = Uploader.UploadPage.LastIndexOf("&ContentType=");
            Uploader.UploadPage = Uploader.UploadPage.Substring(0, idx) + string.Concat("&ContentType=", selectedType);
        }
        private IEnumerable<string> GetFileContentTypesNames()
        {
            // query Content Types, that inherit from the File Content Type
            var settings = new QuerySettings { EnableAutofilters = false, Sort = new SortInfo[] { new SortInfo { FieldName = "Name" }} };
            var fileTypePath = RepositoryPath.Combine(Repository.ContentTypesFolderPath, ActiveSchema.NodeTypes["File"].NodeTypePath);
            var nodes = ContentQuery.Query(string.Format("+Type:ContentType +InTree:{0}", fileTypePath), settings).Nodes;
 
            return nodes.Select(n => n.Name);
        }
    }
}

There are a few things to note here:

  • our control inherits from System.Web.UI.UserControl,
  • we use the FindControlRecursive method defined in SenseNet.Portal.UI.Controls to find a control in the markup,
  • we use the UploadHelper class in using SenseNet.Portal.Handlers to add the Auto Content Type to the dropdown,
  • we use some Repository, RepositoryPath and ActiveSchema class functions to get the File Content Type location,
  • we use Content Query to get all Content Types, that are derived from the File Content Type.

3. Create the portlet to host the control

In this step we will create a portlet to host our control we have just created. We are going to create the following file for the portlet code:

  • Sl2FileUpload\Sl2FileUpload\UploaderPortlet.cs

The portlet will load our control, and render it. It will also make some permission checks to see if the current user can actually upload to the target folder if there are no Allowed Content Types set (if Allowed Content Types property on a folder is not set, the user can upload anything, even .ascx controls that can be used later to deface the portal, and therefore for security reasons only administrators should be allowed to upload to folders without Content Type restrictions). The portlet will be Context Bound, and therefore can be used later on Application pages, so that the user will be able to upload files to any folder with just a single click.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using SenseNet.Portal.UI.PortletFramework;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI;
using System.IO;
using System.Text;
using System.Xml;
using SenseNet.ContentRepository;
using SenseNet.ContentRepository.Storage;
using SenseNet.ContentRepository.Storage.Security;
using SenseNet.Portal.UI.Controls;
using SenseNet.Portal.Virtualization;
using System.ComponentModel;
 
namespace Sl2FileUpload
{
    public class Sl2FileUploadPortlet : ContextBoundPortlet
    {
        private string _viewPath = "/Root/System/SystemPlugins/Controls/Sl2FileUploadControl.ascx";
        [WebBrowsable(true), Personalizable(true)]
        [WebDisplayName("Control view path")]
        [WebDescription("Path of the control which renders the SilverLight control")]
        [WebCategory(EditorCategory.UI, EditorCategory.UI_Order)]
        [WebOrder(100)]
        [Editor(typeof(ContentPickerEditorPartField), typeof(IEditorPartField))]
        [ContentPickerEditorPartOptions(ContentPickerCommonType.Ascx)]
        public string ViewPath
        {
            get { return _viewPath; }
            set { _viewPath = value; }
        }
 
 
        public Sl2FileUploadPortlet()
        {
            this.Name = "Uploader portlet (SilverLight 2)";
            this.Description = "This Portlet renders a SilverLight 2 compatible multiple file upload control (context bound)";
            this.Category = new PortletCategory(PortletCategoryType.Application);
        }
        protected override void CreateChildControls()
        {
            if (ContextNode == null)
            {
                this.Controls.Add(new LiteralControl("ContextNode is null."));
                return;
            }
            if (!AllowCreationForEmptyAllowedContentTypes(ContextNode.Path))
            {
                this.Controls.Add(new LiteralControl("Allowed ContentTypes list is empty!"));
                return;
            }
 
            try
            {
                var control = this.Page.LoadControl(ViewPath) as Sl2FileUploadControl;
                control.ContextNode = ContextNode;
                this.Controls.Add(control);
            }
            catch (Exception e)
            {
                this.Controls.Add(new LiteralControl(e.ToString()));
            }
        }
        protected bool AllowCreationForEmptyAllowedContentTypes(string parentPath)
        {
            // if allowed content types list is empty, only administrators should be able to use this portlet
            if (!string.IsNullOrEmpty(parentPath))
            {
                var parent = Node.LoadNode(parentPath) as GenericContent;
                if (parent != null)
                {
                    if (parent.ContentTypes.Count() == 0 && !PortalContext.Current.ArbitraryContentTypeCreationAllowed)
                        return false;
                }
            }
            return true;
        }
    }
}

4. Create an action to allow easy acces from portal

Everything is ready now to be visualized on a page. Let's create a new Application page:

  • install the created portlet:
    • copy Sl2FileUpload.dll (and references) to webroot bin folder
    • Explore /Root/Portlets and invoke Synchronize action, install all portlets (Uploader portlet should also appear in list)
  • create a Portlet Page under /Root/(apps)/Folder with the name SilverLight Upload:
    • PageTemplate can be sn-layout-dialog,
    • Scenario can be ListItem;ExploreActions to display it at common places.
  • add the portlet to the page
    • Browse the created page and then using the PRC switch to edit mode,
    • Add Uploader portlet (Application category) to Wide Column,
    • Edit the portlet and give it a title, ie: Multi-file upload
    • Check-in page with PRC.

We are ready, let's see how it works! In Explore, go to /Root/Sites/Default_Site. In the 'Actions' menu, a new item called SilverLight Upload should appear. Click it! Now the application page should appear with the new SilverLight uploader control:

SilverLight Upload Application

If you want to completely change the existing Flash uploader to this one, simply edit the /Root/(apps)/Folder/Upload Portlet Page, remove the portlets from it and add the SilverLight Uploader portlet. From then onwards the existing Upload action will use the SilverLight uploader instead of the Flash version everywhere.

5. Create the handler to process uploaded files

The user interface is ready to work, now comes the tricky part! We are going to create the handler that will process the uploaded files and put them into the Content Repository. We will create the following files:

  • Sl2FileUpload\Root\System\WebRoot\Sl2FileUpload.ashx
  • Sl2FileUpload\Sl2FileUpload\Sl2FileUploadHandler.cs

The .ashx file is just an entry point for the handler:

<%@ WebHandler Language="C#" Class="Sl2FileUpload.Sl2FileUploadHandler" %>

It is useful that it is in the Content Repository, because this way you can fine-adjust permissions for accessing the upload feature. The code-behind for the handler will contain the following things:

  • extract uploaded file parameters like target folder id, filename, file send completed,
  • save the file to filesystem to a dedicated folder,
  • when the send is completed, moves the file to the Content Repository, to the appropriate location,
  • if the node already exists, it performs a modify action on the existing node,
  • if Auto has been selected, Content Type will be determined using extension,
  • if Content Type cannot be resolved, File type will be used.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using DC.SilverlightFileUpload;
using System.IO;
using SenseNet.ContentRepository.Storage;
using SenseNet.Diagnostics;
using SenseNet.Portal.Virtualization;
 
namespace Sl2FileUpload
{
    public class Sl2FileUploadHandler : IHttpHandler
    {
        // ============================================================================ Consts
        private const string PARAMFILENAME = "filename=";
        private const string PARAMCOMPLETE = "complete";
        private const string PARAMGETBYTES = "getbytes";
        private const string PARAMSTARTBYTE = "startbyte";
        private const string PARAMUPLOADERTOKEN = "uploadertoken";
        private const string PARAMPARENTID = "parentid";
        private const string PARAMCONTENTTYPE = "contenttype";
 
 
        // ============================================================================ IHttpHandler
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
        public void ProcessRequest(HttpContext context)
        {
            string uploadPath = context.Server.MapPath("~/Upload");
            Process(context, uploadPath);
        }
 
 
        // ============================================================================ Private methods
        private string TrimParamFromEnd(string s, string param)
        {
            var idx = s.ToLower().LastIndexOf("&"+param);
            if (idx != -1)
                return s.Substring(0, idx);
            return s;
        }
        private string FileName
        {
            get
            {
                var queryString = HttpUtility.UrlDecode(HttpContext.Current.Request.Url.Query);
 
                // trim getbytes, startbyte and complete parameters from end
                queryString = TrimParamFromEnd(queryString, PARAMCOMPLETE);
                queryString = TrimParamFromEnd(queryString, PARAMGETBYTES);
                queryString = TrimParamFromEnd(queryString, PARAMSTARTBYTE);
 
                // trim ?filename
                var idx = queryString.ToLower().IndexOf(PARAMFILENAME);
                if (idx == -1)
                    return null;
 
                // remove invalid characters
                var fileName = queryString.Substring(idx + PARAMFILENAME.Length);
                foreach (var ch in RepositoryPath.InvalidNameChars)
                {
                    fileName = fileName.Replace(ch.ToString(), "");
                }
                return fileName;
            }
        }
        private void Process(HttpContext context, string uploadPath)
        {
            bool complete = string.IsNullOrEmpty(context.Request.QueryString[PARAMCOMPLETE]) ? true : bool.Parse(context.Request.QueryString[PARAMCOMPLETE]);
            bool getBytes = string.IsNullOrEmpty(context.Request.QueryString[PARAMGETBYTES]) ? false : bool.Parse(context.Request.QueryString[PARAMGETBYTES]);
            long startByte = string.IsNullOrEmpty(context.Request.QueryString[PARAMSTARTBYTE]) ? 0 : long.Parse(context.Request.QueryString[PARAMSTARTBYTE]);
 
            var filePath = Path.Combine(uploadPath, string.Format("{0}_{1}", context.Request.QueryString[PARAMUPLOADERTOKEN], this.FileName));
 
            if (getBytes)
            {
                FileInfo fi = new FileInfo(filePath);
                if (!fi.Exists)
                    context.Response.Write("0");
                else
                    context.Response.Write(fi.Length.ToString());
 
                context.Response.Flush();
                return;
            }
            else
            {
                var append = startByte > 0 && File.Exists(filePath);
                using (FileStream fs = append ? File.Open(filePath, FileMode.Append) : File.Create(filePath))
                {
                    SaveFile(context.Request.InputStream, fs);
                    fs.Close();
                }
                if (complete)
                    MoveToRepository(filePath);
            }
        }
        private void SaveFile(Stream stream, FileStream fs)
        {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
            {
                fs.Write(buffer, 0, bytesRead);
            }
        }
        private void MoveToRepository(string filePath)
        {
            var parentId = Convert.ToInt32(HttpContext.Current.Request.QueryString[PARAMPARENTID]);
            var contentTypeName = HttpContext.Current.Request.QueryString[PARAMCONTENTTYPE];
 
            try
            {
                var parent = Node.LoadNode(parentId);
                if (parent == null)
                    throw new InvalidOperationException(string.Format("Target parent node (id: {0}) could not be loaded.", parentId));
 
                var targetPath = RepositoryPath.Combine(parent.Path, this.FileName);
                var actualContentTypeName = UploadHelper.GetContentType(this.FileName, contentTypeName);
                actualContentTypeName = string.IsNullOrEmpty(actualContentTypeName) ? "File" : actualContentTypeName;
 
                using (FileStream fs = File.OpenRead(filePath))
                {
                    var existingNode = Node.LoadNode(targetPath);
                    if (existingNode == null)
                        UploadHelper.CreateNodeOfType(actualContentTypeName, parent, this.FileName, fs);
                    else
                        UploadHelper.ModifyNode(existingNode, fs);
                }
                File.Delete(filePath);
            }
            catch (Exception ex)
            {
                Logger.WriteException(ex);
                throw;
            }
        }
    }
}

The tricky part is the MoveToRepository function. Up until that, it's regular IHttpHandler processing and File saving, and is very similar to the sample codes that you can find for third-party uploader tools. So let's see how MoveToRepository works:

  • first we load the parent folder according to the parameters passed,
  • we use the UploadHelper class to determine the type to be created,
  • we determine the targetPath using the RepositoryPath class, to see if node already exists,
  • we try to load the node at targetPath to see if it already exists,
  • we use the UploadHelper class methods to persist the file to the Content Repository,
  • we delete the file from the file system.

We are ready, create an Upload folder in the web folder, and you can start uploading files.

6. Deploying the solution

In the previous steps we actually did deploy the final solution, but let's see the big picture and suppose, we have created the files and the project, and now we want to deploy the solution. This is how it goes:

  • copy dll's to webfolder bin:
    • Sl2FileUpload.dll
    • DC.SilverlightFileUpload.dll
    • System.Web.Silverlight.dll
  • upload necessary files to Content Repository:
    • /Root/System/SystemPlugins/Controls/Sl2FileUploadControl.ascx
    • /Root/System/WebRoot/Sl2FileUpload.ashx -> this might be tricky to upload, you can just create an Empty Content View.ascx, copy the contents and rename it to Sl2FileUpload.ashx
  • install the portlet and create Application page, as described in step 4,
  • create a folder in the webfolder in file system with the name Upload and grant permissions to the user running the application pool of the site, so that it can save and delete files in it!

Video

Related links

References