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
- 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)
{
DropUIElement = (UIElement)param;
DropUIElementOpacity = DropUIElement.Opacity;
DropUIElement.AllowDrop = true;
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)
{
if (FileUploadingProperty == false)
{
DropUIElement.Opacity = (double)0.5;
}
}
void DropUIElement_DragLeave(object sender, DragEventArgs e)
{
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)
{
if (FileUploadingProperty == false)
{
DropUIElement.Opacity = DropUIElementOpacity;
if (e.Data != null)
{
FileInfo[] Dropfiles = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];
files = new ObservableCollection();
string strURLWithSelectedFolder = string.Format("{0}?folder={1}", GetWebserviceAddress(), SelectedSilverlightFolder.FullPath);
Uri uri = new Uri(strURLWithSelectedFolder, UriKind.Absolute);
UploadUrl = uri;
foreach (FileInfo fi in Dropfiles)
{
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;
}
upload.StatusChanged += new EventHandler(upload_StatusChanged);
upload.UploadProgressChanged += new ProgressChangedEvent(upload_UploadProgressChanged);
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)
{
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;
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);
});
}
}
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)
{
string strfolder = context.Request.QueryString["folder"];
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 (fi.Exists)
{
try
{
fi.Delete();
}
catch
{
}
}
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.