Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight 4 Drag and Drop File Manager

0.00/5 (No votes)
6 May 2010 3  
A Silverlight file manager that allows drag and drop multiple file uploads

A Silverlight 4 Drag and Drop File Manager 

This is part 2 to the article Silverlight File Manager. That article focused on the View Model Style pattern, and how it supports Designer / Developer collaboration and workflow when using Microsoft Expression Blend 4 (or higher). This time we will cover some of the "hard stuff" by implementing drag and drop with real-time file upload progress notification. However, implementing this will be surprisingly easy.

The View Model Style pattern allows a programmer to create an application that has absolutely no UI (user interface). The programmer only creates a ViewModel and a Model. A designer with no programming ability at all, is then able to start with a blank page and completely create the View (UI) in Microsoft Expression Blend 4 (or higher). If you are new to View Model Style it is suggested that you read Silverlight View Model Style: An (Overly) Simplified Explanation for an introduction.

We will also demonstrate how, by passing UI elements to the ViewModel, you can address any programming challenge easily. You can achieve full Microsoft Expression Blendability (the ability to use Expression Blend to create a UI with no code), by wiring-up events to UI elements as needed.

Taking The Hand Off From Alan Beasley

This project opens up from where Alan Beasley's article: ListBox Styling (Part2-ControlTemplate) in Expression Blend & Silverlight ends. That article begins where Silverlight View Model Style File Manager ended. In Silverlight View Model Style File Manager the file manager looked like this:

After his article, ListBox Styling (Part2-ControlTemplate) in Expression Blend & Silverlight, the file manager looked like this:

As you can see, this is more than a "slight" improvement. He did this without touching the code at all, and he was free to actually change any UI elements that he wanted to.

Designers: Can't Live Without Them...

There is a price to pay when you turn over your UI to a Designer. They become as protective about their design as the programmer is about their code. This project actually went through a lot of changes by Alan and I. We came to an easy agreement. I wont touch anything with a ".xaml" extension (UI and style elements) and he wont touch anything with a ".cs" extension (code).

Over the past few weeks, we have proven this actually works. View Model Style allows us to have this "separation of concerns". He can email a .xaml file to me and I simply drop it in to my version of the project and "oh the design just changed".

Here is a recent exchange when I "crossed the line", when thought I was "helping out" by applying a style to a UI element on my own:

Me: "You made me laugh out loud at your comment "That abomination you created..." Because you're right! I initially thought it was ok until you said something."
Alan: "You may laugh, but I almost died when I saw that through bleary eyes this morning! Noooooooooooo!!!!!!!!"

The Drag And Drop File Manager

This article covers the addition of the file upload feature. It provides the ability to upload a file by simply dropping it on the folder details panel (on the right hand side of the application). This is what the folder details looks like when you are viewing the files in the selected folder:

When you drag a file over the panel it changes color:

When you drop file(s) on the panel a progress bar will appear:

After the upload process is complete, the uploaded file(s) will immediately show. You can simply click on a file to download it:

The Designer: Implementing Drag And Drop

The Programmer makes changes to the code (any file except files with the .xaml extension), and provides the updated files to the Designer. If you have source control you would simply check in the changes and the Designer would check them out. Note that Expression Blend works with Team Foundation Server source control.

To implement the UI for the upload functionality, the Designer opens up the project in Microsoft Expression Blend 4 (or higher), and implements the following:

  • Indicates what element is to be used as a "drop point" to upload files
  • Indicates what control will be used as an "upload indicator"

The "Drop Point" Element

The Designer gets an InvokeCommand behavior...

... and drags it under the LayoutRoot in the Objects and Timeline window.

In the Properties for the behavior, the Designer sets the EventName to Loaded. This means that the behavior will fire when the LayoutRoot is first loaded. This event is useful for "setting things up" such as registering a "drop point".

Next, the Designer selects the DataBind button next to Command

The Command is bound to the SetDropUIElementCommand.

Note, this is a common technique when implementing "Simplified View Model Style". We need to hook into the mouse over and drop events on the element. This is normally easy to to implement in code behind. With View Model Style you do not want to use code behind (because the Designer is not able to easily use Blend to alter the design without touching the code). So, we simply "register" the element when the application first loads, by passing the element as a parameter using a behavior. The SetDropUIElementCommand method wire-ups handlers for the events that we need.

Some would argue that this is tying the ViewModel to the View. However, we are simply just passing a parameter. The SetDropUIElementCommand, that accepts the parameter, does not know exactly what element will be used as a drop point. In this case, the parameter type is UIElement. The View by definition is composed of UIElements. The Designer could use a ScrollBox initially and later change it to a rectangle without any code changes.

To indicate what element will be used for the drop point, the Advanced options button next to CommandParameter is clicked.

The FileDetails (ScrollViewer) is indicated as the parameter. 

The File Upload Progress Indicator

The Designer grabs a BusyIndicator control...

...and binds it to the IsBusy property of the control to the FileUploadingProperty, and the BusyContent property to the FileuploadPercentProperty.

The Designer hits F5 to run the project, and the application is complete!

The Code

The Programmers job is a bit more involved, these are the basic changes that I needed to make:

  • Silverlight Code
    • Allow the Designer to specify the UIElement to use as drop point for file uploads
      • Changing opacity of the UIElement to indicate drop point
      • Starting FileUpload, and wiring-up events
        • Upload progress
          • Updating FileUploadingProperty
          • Updating FileUploadPercentProperty
        • Upload completed
          • Refresh file folder
  • Website Code
    • Upload code (when the FileUpload class calls the .ashx file in the website)

The File Uploader Code

For the file uploading, I decided to use code from the Silverlight file Upload project at: http://silverlightfileupld.codeplex.com (by darick_c). I have used this code several times before, for modules on DNN Silverlight. In the original project, he has a full featured upload control. The management of this upload client consumes most of the code in the original solution.

The objective that I had was to reduce the code, to just the elements required for uploading files, and to restructure it so that it works with the View Model Style pattern.

Element To Use As a Drop Point

Here is the complete code used to allow the Designer to register a UIElement as a drop point:

        #region SetDropUIElementCommand
        public ICommand SetDropUIElementCommand { get; set; }
        public void SetDropUIElement(object param)
        {
            // Set the UI Element to be the drop point for files
            DropUIElement = (UIElement)param;

            // Save the opacity of the element
            DropUIElementOpacity = DropUIElement.Opacity;

            // Turn on allow drop
            DropUIElement.AllowDrop = true;

            // Attach event handlers
            DropUIElement.DragOver += new DragEventHandler(DropUIElement_DragOver);
            DropUIElement.DragLeave += new DragEventHandler(DropUIElement_DragLeave);
            DropUIElement.Drop += new DragEventHandler(DropUIElement_Drop);
        }

        private bool CanSetDropUIElement(object param)
        {
            return true;
        }

        void DropUIElement_DragOver(object sender, DragEventArgs e)
        {
            // Only allow drop if not uploading
            if (FileUploadingProperty == false)
            {
                // If you hover over the drop point, change it's opacity so users will 
                // have some indication that they can drop
                DropUIElement.Opacity = (double)0.5;
            }
        }

        void DropUIElement_DragLeave(object sender, DragEventArgs e)
        {
            // Return opacity to normal
            DropUIElement.Opacity = DropUIElementOpacity;
        }        
        #endregion

It attaches 3 handlers to the element:

  • DragOver - Calls DropUIElement_DragOver that changes the opacity to 0.5, so the user will know this is a drop point.
  • DragLeave - Calls DropUIElement_DragLeave that returns the opacity to normal.
  • Drop - Calls DropUIElement_Drop that then calls the method to upload the file(s).

Here is the code for DropUIElement_Drop:

        #region DropUIElement_Drop
        void DropUIElement_Drop(object sender, DragEventArgs e)
        {
            // Only allow drop if not uploading
            if (FileUploadingProperty == false)
            {
                // Return opacity to normal
                DropUIElement.Opacity = DropUIElementOpacity;

                // If there is something being dropped upload it
                if (e.Data != null)
                {
                    FileInfo[] Dropfiles = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];
                    files = new ObservableCollection();

                    // Get the upload URL
                    string strURLWithSelectedFolder = string.Format("{0}?folder={1}", GetWebserviceAddress(), SelectedSilverlightFolder.FullPath);
                    Uri uri = new Uri(strURLWithSelectedFolder, UriKind.Absolute);
                    UploadUrl = uri;

                    foreach (FileInfo fi in Dropfiles)
                    {
                        // Create an FileUpload object
                        FileUpload upload = new FileUpload(App.Current.RootVisual.Dispatcher, UploadUrl, fi);

                        if (UploadChunkSize > 0)
                        {
                            upload.ChunkSize = UploadChunkSize;
                        }

                        if (MaximumTotalUpload >= 0 && TotalUploadSize + upload.FileLength > MaximumTotalUpload)
                        {
                            MessageBox.Show("You have exceeded the total allowable upload amount.");
                            break;
                        }

                        if (MaximumUpload >= 0 && upload.FileLength > MaximumUpload)
                        {
                            MessageBox.Show(string.Format("The file '{0}' exceeds the maximun upload size.", upload.Name));
                            break;
                        }

                        // Wire up handles for status changed and upload percentage
                        // These will be updating the properties that the ViewModel exposes
                        upload.StatusChanged += new EventHandler(upload_StatusChanged);
                        upload.UploadProgressChanged += new ProgressChangedEvent(upload_UploadProgressChanged);

                        // Start the Upload
                        upload.Upload();
                    }
                }
            }
        } 
        #endregion

This code does the following:

  • Gets the URL to use to upload the file
  • Calls FileUpload class passing:
    • App.Current.RootVisual.Dispatcher - The current Dispatcher of the application. The FileUpload object requires the Dispatcher to wire up delegates.
    • UploadUrl - The URL to upload the current file to.
    • fi - The current file to upload.

It also wires up the following events:

  • upload.StatusChanged - Calls upload_StatusChanged to track when the file upload is completed.
  • upload.UploadProgressChanged - Calls upload_UploadProgressChanged to track the upload progress.

When the FileUpload status changes (for example when it is complete) the following method is raised.

        #region upload_StatusChanged
        void upload_StatusChanged(object sender, EventArgs e)
        {
            FileUpload fu = sender as FileUpload;

            FileUploadingProperty = (fu.Status == FileUploadStatus.Uploading);

            if (fu.Status == FileUploadStatus.Complete)
            {
                 // Refresh files for the selected folder
                SetFiles(SelectedSilverlightFolder);
            }
        } 
        #endregion

If the upload is complete, the SetFiles(SelectedSilverlightFolder) call refreshes the current list of files, and causes the uploaded file(s) to display in the file list.

The File Upload Class

This class is the original class created by darick_c. All I did was strip it down considerably. There is still a lot of code that could be still stripped out, because the class no longer has to manage the UI of the original project. However, I stripped it down enough that others should have a fairly easy time adapting this for their own uses.

In the Uploader class, here is the code that does most of the heavy lifting:

        public void UploadFileEx()
        {
            Status = FileUploadStatus.Uploading;
            long temp = FileLength - BytesUploaded;

            UriBuilder ub = new UriBuilder(UploadUrl);
            bool complete = temp <= ChunkSize;
            ub.Query = string.Format("{3}filename={0}&StartByte={1}&Complete={2}", 
                File.Name, BytesUploaded, complete, string.IsNullOrEmpty(ub.Query) ? "" : ub.Query.Remove(0, 1) + "&");

            HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(ub.Uri);
            webrequest.Method = "POST";
            webrequest.BeginGetRequestStream(new AsyncCallback(WriteCallback), webrequest);
        }

        private void WriteCallback(IAsyncResult asynchronousResult)
        {
            HttpWebRequest webrequest = (HttpWebRequest)asynchronousResult.AsyncState;
            // End the operation.
            Stream requestStream = webrequest.EndGetRequestStream(asynchronousResult);

            byte[] buffer = new Byte[4096];
            int bytesRead = 0;
            int tempTotal = 0;

            Stream fileStream = resizeStream != null ? (Stream)resizeStream : File.OpenRead();

            fileStream.Position = BytesUploaded;
            while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0 && tempTotal + bytesRead < ChunkSize && !cancel)
            {
                requestStream.Write(buffer, 0, bytesRead);
                requestStream.Flush();
                BytesUploaded += bytesRead;
                tempTotal += bytesRead;
                if (UploadProgressChanged != null)
                {
                    int percent = (int)(((double)BytesUploaded / (double)FileLength) * 100);
                    UploadProgressChangedEventArgs args = 
                        new UploadProgressChangedEventArgs(percent, bytesRead, BytesUploaded, FileLength, file.Name);
                    this.Dispatcher.BeginInvoke(delegate()
                    {
                        UploadProgressChanged(this, args);
                    });
                }
            }

            // only close the stream if it came from the file, don't close resizestream so we don't have to resize it over again.
            if (resizeStream == null)
                fileStream.Close();
            requestStream.Close();
            webrequest.BeginGetResponse(new AsyncCallback(ReadCallback), webrequest);
        }

        private void ReadCallback(IAsyncResult asynchronousResult)
        {
            HttpWebRequest webrequest = (HttpWebRequest)asynchronousResult.AsyncState;
            HttpWebResponse response = (HttpWebResponse)webrequest.EndGetResponse(asynchronousResult);
            StreamReader reader = new StreamReader(response.GetResponseStream());

            string responsestring = reader.ReadToEnd();
            reader.Close();

            if (cancel)
            {
                if (resizeStream != null)
                    resizeStream.Close();
                if (remove)
                    Status = FileUploadStatus.Removed;
                else
                    Status = FileUploadStatus.Canceled;
            }
            else if (BytesUploaded < FileLength)
                UploadFileEx();
            else
            {
                if (resizeStream != null)
                    resizeStream.Close();

                Status = FileUploadStatus.Complete;
            }
        }

Basically it uploads the file chunk by chunk. The careful management of this process is the beauty of this class. Any credit for this belongs entirely to darick_c. If it has any problems it was surely introduced by me. There are others ways to handle uploading files, but I have used this code for years, on a number of different projects, and this code works great for me.

The Web Server Code

The web server code is more strait-forward. Basically it communicates with the FileUpload class. Most of this code is from the original darick_c project. Again, I simplified it so it would be easier to customize. The code for the .ascx file on the server is as follows:

        private HttpContext ctx;
        public void ProcessRequest(HttpContext context)
        {
            ctx = context;
            FileUploadProcess fileUpload = new FileUploadProcess();
            fileUpload.FileUploadCompleted += new FileUploadCompletedEvent(fileUpload_FileUploadCompleted);
            fileUpload.ProcessRequest(context);
        }

        void fileUpload_FileUploadCompleted(object sender, FileUploadCompletedEventArgs args)
        {            
            string id = ctx.Request.QueryString["id"];
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }

That code calls ProcessRequest, to process the upload:

    #region class FileUploadProcess
    public class FileUploadProcess
    {
        public event FileUploadCompletedEvent FileUploadCompleted;

        #region ProcessRequest
        public void ProcessRequest(HttpContext context)
        {
            // ** Selected Folder is passed in the Header
            string strfolder = context.Request.QueryString["folder"];

            // Other values passed

            string Originalfilename = string.IsNullOrEmpty(context.Request.QueryString["filename"]) 
                ? "Unknown" : context.Request.QueryString["filename"];
            bool complete = string.IsNullOrEmpty(context.Request.QueryString["Complete"]) 
                ? true : bool.Parse(context.Request.QueryString["Complete"]);
            bool getBytes = string.IsNullOrEmpty(context.Request.QueryString["GetBytes"]) 
                ? false : bool.Parse(context.Request.QueryString["GetBytes"]);
            long startByte = string.IsNullOrEmpty(context.Request.QueryString["StartByte"]) 
                ? 0 : long.Parse(context.Request.QueryString["StartByte"]); ;

            string strExtension = System.IO.Path.GetExtension(Originalfilename);
            string strFileDirectory = context.ApplicationInstance.Server.MapPath(@"~\Files\");
            strFileDirectory = strFileDirectory + @"\" + strfolder;
            string filePath = Path.Combine(strFileDirectory, Originalfilename);

            if (getBytes)
            {
                FileInfo fi = new FileInfo(filePath);

                // If file exists - delete it
                if (fi.Exists)
                {
                    try
                    {
                        fi.Delete();
                    }
                    catch
                    {
                        // could not delete
                    }
                }

                context.Response.Write("0");
                context.Response.Flush();
                return;
            }
            else
            {
                if (startByte > 0 && File.Exists(filePath))
                {
                    using (FileStream fs = File.Open(filePath, FileMode.Append))
                    {
                        SaveFile(context.Request.InputStream, fs);
                        fs.Close();
                    }
                }
                else
                {
                    using (FileStream fs = File.Create(filePath))
                    {
                        SaveFile(context.Request.InputStream, fs);
                        fs.Close();
                    }
                }
                if (complete)
                {
                    if (FileUploadCompleted != null)
                    {
                        FileUploadCompletedEventArgs args = new FileUploadCompletedEventArgs(Originalfilename, filePath);
                        FileUploadCompleted(this, args);
                    }
                }
            }
        }
        #endregion

        #region SaveFile
        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);
            }
        }
        #endregion
    }
    #endregion

View Model Style Simplified

By passing UI elements to the ViewModel using ICommands and wiring-up events to these elements as needed, you can achieve full Microsoft Expression Blendability and testing. Want to test an ICommand that requires a UIElement as a parameter? Pass it a UIElement, raise the event in your test method, and compare the expected result.

When you pass UI elements as parameters, you can hook up any event such as MouseDown.This allows you to respond to a direct action, rather than attempting to infer an intent in the View purely through bindings. While this may seem like you are tying your ViewModel to your View, the actual UI element being passed can easily be changed by the Designer. In this way, you can provide the Designer the greatest flexibility to truly "design" the application, not just "style" it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here