The simplest way to upload files to a Lightswitch application

Introduction

In the previous post I covered downloading files. In order to be able to test this we need upload functionality.

There are 2 options:

  1. Upload the file via the table mechanism
  2. Upload the file via web-api

Here is the sample solution: http://code.msdn.microsoft.com/vstudio/Simple-web-api-based-file-5e0c5844

Upload a file via the table mechanism

Since I’m a plumber, I leave fancy UI stuff to more UI talented people. My upload screen, simply has a label and a button. The label is for displaying the file name and the button is for doing the file selection:

uploadviatablescreen

 

The button is a custom control but a very simple one, no xaml here.

button

The reason why we add a button as custom control is for accommodating to an annoying Silverlight security restriction, which I will not try to fully explain over here.

 

The view model looks as follows:

tableviewmodel

 

The code behind is a bit more involved:

using System;
using System.Linq;
using System.IO;
using System.IO.IsolatedStorage;
using System.Collections.Generic;
using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Framework.Client;
using Microsoft.LightSwitch.Presentation;
using Microsoft.LightSwitch.Presentation.Extensions;
using System.Windows.Browser;
using Microsoft.LightSwitch.Threading;
using LightSwitchApplication.Poco;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows;
using System.Windows.Data;
using Microsoft.VisualStudio.ExtensibilityHosting;
using Microsoft.LightSwitch.Sdk.Proxy;
using System.Windows.Markup;
namespace LightSwitchApplication
{
public partial class UploadViaTable
{//ingredients:
//The viewmodel needs a string property “SelectedFileName” which is NOT a parameter and is NOT required
//the property is bound to  a Label control on the screen.
//next to the label a custom control ( a simple system.windows.controls.button) is placed.

partial void UploadViaTable_Created()
{
SelectedFileName = “Please select a file”;
var fileDialogCustomControl = this.FindControl(“ReadLocalFileContentButton”);
fileDialogCustomControl.ControlAvailable += (s, e) =>
{
Button button = e.Control as Button;
button.Content = “…”;
button.Click -= FileDialogButton_Click;
button.Click += FileDialogButton_Click;
};
}

private void FileDialogButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
Button button = sender as Button;
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == false)
{
SelectedFileName = string.Empty;

}
else
{
byte[] binaryData = null;
SelectedFileName = “Handling the file upload. Please wait….”;
string fileName = string.Empty;
try
{
using (FileStream fileStream = openFileDialog.File.OpenRead())
{
fileName = openFileDialog.File.Name;

using (BinaryReader streamReader = new BinaryReader(fileStream))
{
binaryData = streamReader.ReadBytes((int)fileStream.Length);
}

}
}
catch (IOException)
{

this.Details.Dispatcher.BeginInvoke(() =>
{
this.ShowMessageBox(“IO error…”);
SelectedFileName = “An error occured”;
}
);
}

this.Details.Dispatcher.BeginInvoke(() =>
{
//we simple take the file is associated with the first customer (just an example)
//it will not be possible to associate files to records not available as a result of the row level security rules
//this is enforced automatically.
//it’s perfectly possible to assign files to records NOT in the current dataworkspace scope.
//Obviously, attaching files to the screens dataworkspace is possible as well.
var dataService = this.Application.CreateDataWorkspace().ApplicationData;
Customer customer = dataService.Customers.First();
var metaData = customer.FileMetaDatas.AddNew();
metaData.FileName = fileName;
metaData.UploadDateTime = DateTime.Now;
metaData.FileStore = new FileStore(dataService.FileStores); // usage of the overloaded constructor is necessary here !
metaData.FileStore.FileBinaryContent = binaryData;
dataService.SaveChanges();
SelectedFileName = fileName;
});

}

}

partial void DownloadLatestFile_Execute()
{
var dataService = this.Application.CreateDataWorkspace().ApplicationData;
int fileStoreId = dataService.FileStores.OrderByDescending(f => f.Id).First().Id; //as an example we retrieve the latest upload.
//for retrieving the file, we use the FileController’s Download method with as query parameter the id of the FileStore record
Dispatchers.Main.Invoke(() =>
{
Uri baseAddress = new Uri(new Uri(System.Windows.Application.Current.Host.Source.AbsoluteUri), “../../”); //works both in debug and deployed !
string url = string.Format(@”{0}api/File/Download?Id={1}”, baseAddress.AbsoluteUri, fileStoreId);
HtmlPage.Window.Navigate(new Uri(url), “_blank”);
});

}
}
}

You’ll find more context and detailed explanation inline as comment in the code Glimlach

The nice thing about the above approach is that we have almost no server side code at all for this upload functionality.

Note that I have string field which calculates the file size in more readable way. Add the following in the ApplicationDataService class:

partial void FileStores_Inserting(FileStore entity)
{
entity.FileMetaData.FileSize = ConvertBytesToMegabytes(entity.FileBinaryContent.LongCount()).ToString(“0.00″) + ” MB”;
}private double ConvertBytesToMegabytes(long bytes)
{
return (bytes / 1024f) / 1024f;
}

 

 

Nonetheless, there is a restriction: the size of the upload. Very large files (+ 15 mega bytes) are not possible. For very large files, you should use the web-api approach which I’ll cover next.

Upload a file via web-api

Obviously, we need to extend our file controller with following method:

using LightSwitchApplication.Poco;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http;
using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Details;
namespace LightSwitchApplication
{
public class FileController : LightSwitchApiControllerbase
{
[HttpPost]
public HttpResponseMessage Upload(FileUploadRequestParameters requestParameters)
{
FileUploadResponseParameters responseParameters = new FileUploadResponseParameters();using (var ctx = this.Context) //make sure to always put the context in a using !!!
{
var fileMetaData = ctx.DataWorkspace.ApplicationData.FileMetaDatas.AddNew();
IEntitySet entitySet
= ctx.DataWorkspace.ApplicationData.Details.Properties[requestParameters.ReferencedEntitySet].Value
as IEntitySet;
if (entitySet != null)
{
string entityTypeName = entitySet.Details.EntityType.Name;

ICreateQueryMethod query
= ctx.DataWorkspace.ApplicationData.
Details.Methods[entitySet.Details.Name + “_SingleOrDefault”] as ICreateQueryMethod;
if (query != null)
{
object[] keySegment = new object[] { requestParameters.ReferenceId };
ICreateQueryMethodInvocation invocation = query.CreateInvocation(keySegment);
var myvalue = invocation.Execute() as IEntityObject;
fileMetaData.Details.Properties[entityTypeName].Value = myvalue;
fileMetaData.FileName = requestParameters.FileName;
fileMetaData.UploadDateTime = DateTime.Now;
fileMetaData.FileStore = new FileStore();
fileMetaData.FileStore.FileBinaryContent = requestParameters.BinaryData;
ctx.DataWorkspace.ApplicationData.SaveChanges();

responseParameters.UploadStatus = “ok”;
responseParameters.FileSize
= ConvertBytesToMegabytes(fileMetaData.FileStore.FileBinaryContent.LongCount()).ToString(“0.00″) + ” MB”; ;
return Request.CreateResponse<FileUploadResponseParameters>(HttpStatusCode.Accepted, responseParameters);
}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}

}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
}

private double ConvertBytesToMegabytes(long bytes)
{
return (bytes / 1024f) / 1024f;
}

}
}

Since this is a POST command, we’ll need 2 additional very small classes for transporting the parameters going from client to server and visa versa:

namespace LightSwitchApplication.Poco
{
public class FileUploadRequestParameters
{
public string FileName { get; set; }
public byte[] BinaryData { get; set; }
public string ReferencedEntitySet { get; set; }
public int ReferenceId { get; set; }
}

}

namespace LightSwitchApplication.Poco
{
public class FileUploadResponseParameters
{
public string UploadStatus { get; set; }
public string FileSize { get; set; }
public string ReferencedEntitySet { get; set; }
public int? ReferenceId { get; set; }
}
}

Add these classes (as a link) also to the silverlight client project.

 

The client side for the web-api based file upload

 

The viewmodel of our upload screen is very similar to the previous one. We need one additional control, a busy indicator. Therefor, we have an additional boolean property IsBusy on our view model:

webapiviewmodel

 

 

Of course, you need to add the Busy Indicator as a custom control:

busy

 

The code behind will be a bit more extended than the previous approach, because we need to add the handling for the busy indicator (when the upload is taking place):

using System;
using System.Linq;
using System.IO;
using System.IO.IsolatedStorage;
using System.Collections.Generic;
using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Framework.Client;
using Microsoft.LightSwitch.Presentation;
using Microsoft.LightSwitch.Presentation.Extensions;
using System.Windows.Browser;
using Microsoft.LightSwitch.Threading;
using LightSwitchApplication.Poco;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows;
using System.Windows.Data;
using Microsoft.VisualStudio.ExtensibilityHosting;
using Microsoft.LightSwitch.Sdk.Proxy;
using System.Windows.Markup;
namespace LightSwitchApplication
{
public partial class WebApiUpload
{//ingredients:
//The viewmodel needs a string property “SelectedFileName” which is NOT a parameter and is NOT required
//the property is bound to  a Label control on the screen.
//next to the label a custom control ( a simple system.windows.controls.button) is placed.
//The viewmdodel needs a boolean property “IsBusy” which is NOT a parameter and is NOT required
//the property is bound to  a custom control on the screen (the busy indicator)
//add next to the button the busyindicator control (necessary files are in referenced assemblies folder.
//system.windows.controls.toolkit.dll –>system.windows.controls.busyindicator
partial void WebApiUpload_Created()
{
IsBusy = false;

SelectedFileName = “Please select a file”;
var fileDialogCustomControl = this.FindControl(“ReadLocalFileContentButton”);
fileDialogCustomControl.ControlAvailable += (s, e) =>
{
Button button = e.Control as Button;
button.Content = “…”;
button.Click -= FileDialogButton_Click;
button.Click += FileDialogButton_Click;
};

var busyProxy = this.FindControl(“FileBusyIndicator”);
//the sole fact that the busyindicator is bound to the IsBusy viewmodel property is not enough (this is just setting up the datacontext of the custom control
//we must still arrange that  the “IsBusyProperty” dependency property of the business indicator is “linked” to the value property of the custom control.
busyProxy.ControlAvailable += ((s, e) =>
{
var busyIndicator = e.Control as BusyIndicator;
var b = new Binding(“Value”);
b.Mode = BindingMode.TwoWay;
busyIndicator.SetBinding(BusyIndicator.IsBusyProperty, b);
});
}

private void FileDialogButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
Button button = sender as Button;
OpenFileDialog openFileDialog = new OpenFileDialog();

string fileName = string.Empty;
byte[] binaryData = null;

if (openFileDialog.ShowDialog() == false)
{
SelectedFileName = string.Empty;

}

else
{
SelectedFileName = “Handling the file upload. Please wait….”;
IsBusy = true;
try
{
using (FileStream fileStream = openFileDialog.File.OpenRead())
{
fileName = openFileDialog.File.Name;
using (BinaryReader streamReader = new BinaryReader(fileStream))
{
binaryData = streamReader.ReadBytes((int)fileStream.Length);
}
fileStream.Close();
}
}

catch (IOException)
{
this.Details.Dispatcher.BeginInvoke(() =>
{
this.ShowMessageBox(“IO error…”);
SelectedFileName = “An error occured”;
IsBusy = false;
});
}
}
//again as an example we attach the file to the first customer
if (binaryData != null)
this.StartWebApiCommand<FileUploadResponseParameters>(“api/File/Upload”,
new FileUploadRequestParameters { BinaryData = binaryData, FileName = fileName, ReferenceId = 1, ReferencedEntitySet = “Customers” },
(error, responseParams) =>
{
IsBusy = false;
SelectedFileName = fileName;

if (error != null || responseParams.UploadStatus != “ok”)
SelectedFileName = “Something went wrong…”;
});

}
partial void DownloadLatestFile_Execute()
{
var dataService = this.Application.CreateDataWorkspace().ApplicationData;
int fileStoreId = dataService.FileStores.OrderByDescending(f => f.Id).First().Id; //as an example we retrieve the latest upload.
//for retrieving the file, we use the FileController’s Download method with as query parameter the id of the FileStore record
Dispatchers.Main.Invoke(() =>
{
Uri baseAddress = new Uri(new Uri(System.Windows.Application.Current.Host.Source.AbsoluteUri), “../../”); //works both in debug and deployed !
string url = string.Format(@”{0}api/File/Download?Id={1}”, baseAddress.AbsoluteUri, fileStoreId);
HtmlPage.Window.Navigate(new Uri(url), “_blank”);
});

}
}
}

Please refer to the inline comment for getting a deeper understanding on how the busy indicator is integrated. The result looks as follows:

busyview

 

I’m using some extension methods which I introduced already in previous articles for the client side web-api handing:

using Microsoft.LightSwitch.Client;
using Microsoft.LightSwitch.Threading;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;namespace LightSwitchApplication
{
public static class LightSwitchCommandProxy
{
public static void StartWebApiCommand<T>(this IScreenObject screen, string commandrelativePath,
object data, Action<Exception, T> callback)
{
Uri baseAddress = null;
baseAddress = GetBaseAddress();

var webRequest = WebRequest.Create(new Uri(baseAddress, commandrelativePath));
webRequest.Method = “POST”;
webRequest.ContentType = “application/json”;

webRequest.BeginGetRequestStream(iar =>
{
var requestStream = webRequest.EndGetRequestStream(iar);

SerializeObject(data, requestStream);

webRequest.BeginGetResponse(iar2 =>
{
WebResponse webResponse;
try
{
webResponse = webRequest.EndGetResponse(iar2);
}
catch (Exception ex)
{
screen.Details.Dispatcher.BeginInvoke(() =>
{
callback(ex, default(T));
});
return;
}
var result = Deserialize<T>(new StreamReader(webResponse.GetResponseStream()));

screen.Details.Dispatcher.BeginInvoke(() =>
{
callback(null, result);
});
}, null);
}, null);
}
//for a get
public static void StartWebApiRequest<T>(this IScreenObject screen, string requestRelativePath,
Action<Exception, T> callback)
{
Uri serviceUri = new Uri(GetBaseAddress(), requestRelativePath);

WebRequest.RegisterPrefix(“http://”,
System.Net.Browser.WebRequestCreator.BrowserHttp);
WebRequest.RegisterPrefix(“https://”,
System.Net.Browser.WebRequestCreator.BrowserHttp);
//out of browser does not work here !
WebRequest webRequest = WebRequest.Create(serviceUri);
webRequest.UseDefaultCredentials = true;
webRequest.Method = “GET”;
webRequest.BeginGetResponse(iar2 =>
{
WebResponse webResponse;
try
{
webResponse = webRequest.EndGetResponse(iar2);
}
catch (Exception ex)
{
screen.Details.Dispatcher.BeginInvoke(() =>
{
callback(ex, default(T));
});
return;
}
var result = Deserialize<T>(new StreamReader(webResponse.GetResponseStream()));

screen.Details.Dispatcher.BeginInvoke(() =>
{
callback(null, result);
});
}, null);

}

public static Uri GetBaseAddress()
{
Uri baseAddress = null;
Dispatchers.Main.Invoke(() =>
{
baseAddress = new Uri(new Uri(System.Windows.Application.Current.Host.Source.AbsoluteUri), “../../”);

});
return baseAddress;
}
private static void SerializeObject(object data, Stream stream)
{
var serializer = new JsonSerializer();
var sw = new StreamWriter(stream);
serializer.Serialize(sw, data);
sw.Flush();
sw.Close();
}

private static T Deserialize<T>(TextReader reader)
{
var serializer = new JsonSerializer();
return serializer.Deserialize<T>(new JsonTextReader(reader));
}
}
}

 

 

Conclusion

Two approaches. The web-api approach is definitely more work but can handle very large file upload and is of course more versatile (e.g. it can be called from any client type).

Happy  coding.