|
Silverlight Multipart File Upload Form Post The communication of Silverlight is mostly based on WCF. What if you want to post something to a PHP or Java server? Or maybe you want to upload a file to the server? Extension MethodsThe extension methods extends HttpWebRequest class to encapsulates the post action. PostFormAsync and PostMultiPartAsync are the two different postings in HTML. One for normal form posting while the other is for file uploading. public static class Extensions { public static void PostFormAsync(this HttpWebRequest request, object parameters, AsyncCallback callback) { request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.BeginGetRequestStream(new AsyncCallback(asyncResult => { Stream stream = request.EndGetRequestStream(asyncResult); DataContractQueryStringSerializer ser = new DataContractQueryStringSerializer(); ser.WriteObject(stream, parameters); stream.Close(); request.BeginGetResponse(callback, request); }), request); } public static void PostMultiPartAsync(this HttpWebRequest request, object parameters, AsyncCallback callback) { request.Method = "POST"; string boundary = "---------------" + DateTime.Now.Ticks.ToString(); request.ContentType = "multipart/form-data; boundary=" + boundary; request.BeginGetRequestStream(new AsyncCallback(asyncResult => { Stream stream = request.EndGetRequestStream(asyncResult); DataContractMultiPartSerializer ser = new DataContractMultiPartSerializer(boundary); ser.WriteObject(stream, parameters); stream.Close(); request.BeginGetResponse(callback, request); }), request); } } SerializersLike the extensions methods, there are two serializers for two form posting. Two classes are almost identical. Both of them can process Dictionary<string, object> objects and classes marked with DataContract. GetCustomAttributesWhen the data is not a dictionary, reflection is used to iterate the fields and properties of an data class. The use the GetCustomAttributes to see if the members are marked with DataMemberAttribute. The Position of the attribute is ignored since it is irrelevant to HTML form posting. foreach (var prop in data.GetType().GetFields()) { foreach (var attribute in prop.GetCustomAttributes(true)) { if (attribute is DataMemberAttribute) { DataMemberAttribute member = attribute as DataMemberAttribute; writer.Write("{0}={1}&", member.Name ?? prop.Name, prop.GetValue(data)); } } } Since query string is just a string, property with complex data type would not be supported, too. How it worksapplication/x-www-form-urlencodedNormal HTML form posting is simple. The body of the HTTP request is actually the query string. Iterating the items in key-value pair is good enough. writer.Write("{0}={1}&", member.Name ?? prop.Name, prop.GetValue(data)); multipart/form-dataFor multi-port form posting, we have to discuss the detail of the HTTP. The example in RFC 1867 will give you some clue:
Content-type: multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="field1"
Joe Blow
--AaB03x
content-disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
All we need to do is define the boundary string boundary = "---------------" + DateTime.Now.Ticks.ToString(); request.ContentType = "multipart/form-data; boundary=" + boundary; Write the boundary to the stream for each property prefixed with extra dashes writer.Write("--"); writer.WriteLine(boundary); For file uploading, add a few more header properties. You can also add the Content-Transfer-Encoding header to specify the base64 or even gzip encoding. To keep the implementation simple, the binary encoding will be used (which is default). Please note that there is no built-in gzip compression supported in Silverlight. External library is needed if you want to compress the data. writer.WriteLine(@"Content-Disposition: form-data; name=""{0}""; filename=""{1}""", key, f.Name); writer.WriteLine("Content-Type: application/octet-stream"); writer.WriteLine("Content-Length: " + f.Length); PHPThe PHP that the Silverlight application posting to is as simple as follows <?php print_r($_REQUEST); $src = $_FILES['y']['tmp_name']; $dest = "C:\\Windows\\Temp\\".$_FILES['y']['name']; echo $src; echo "\r\n"; echo $dest; echo @copy($src, $dest); ?> Page ControlThe page control has a TextBlock to display the result <UserControl x:Class="SilverlightApplication2.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="300"> <Grid x:Name="LayoutRoot" Background="White"> <TextBlock x:Name="output" Foreground="Black"></TextBlock> </Grid> </UserControl> What this control does is asking users to upload a file with open file dialog to http://localhost/test.php and display the result of the page output InitializeComponent(); // Create a request object HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri("http://localhost/test.php")); OpenFileDialog dlg = new OpenFileDialog(); if (dlg.ShowDialog().Value) { request.PostMultiPartAsync(new Dictionary<string, object> { { "x", "1" }, { "y", dlg.File } }, new AsyncCallback(asyncResult => { HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult); Stream responseStream = response.GetResponseStream(); StreamReader reader = new StreamReader(responseStream); this.Dispatcher.BeginInvoke(delegate { output.Text = reader.ReadToEnd(); response.Close(); }); })); } Data Contract UsageGiven you have a Point class [DataContract] public class Point { [DataMember] public int X { get; set; } [DataMember(Name="y")] public int Y { get; set; } } You can post the Point directly request.PostFormAsync(new Point(){ X=1, Y=2 }, new AsyncCallback(asyncResult => ... It will be serialized to X=1&y=2. In this example, X and Y can only be primitive types (string, int, bool, etc.) No nested object. Potential EnhancementCollection type value is not supported. So, if X is an array of int [DataMember] public int[] X { get; set; } It will not be serialized. But if you understand the code, it can be serialized to X=1&X=2&X=0... easily. Final Code (C#)
|