View  Edit  Attributes  History  Attach  Print  Search

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 Methods

The 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);
      }
  }

Serializers

Like 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.

GetCustomAttributes

When 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 works

application/x-www-form-urlencoded

Normal 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-data

For 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);

PHP

The 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 Control

The 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 Usage

Given 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 Enhancement

Collection 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#)

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Net;
  4. using System.Windows.Controls;
  5. using System.IO;
  6. using System.Runtime.Serialization;
  7.  
  8. namespace SilverlightApplication2
  9. {
  10.     public partial class Page : UserControl
  11.     {
  12.         public Page()
  13.         {
  14.             InitializeComponent();
  15.             // Create a request object  
  16.             HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri("http://localhost/test.php"));
  17.             OpenFileDialog dlg = new OpenFileDialog();
  18.             if (dlg.ShowDialog().Value)
  19.             {
  20.                 request.PostMultiPartAsync(new Dictionary<string, object> { { "x", "1" }, { "y", dlg.File } }, new AsyncCallback(asyncResult =>
  21.                 {
  22.                     HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
  23.  
  24.                     Stream responseStream = response.GetResponseStream();
  25.                     StreamReader reader = new StreamReader(responseStream);
  26.                     this.Dispatcher.BeginInvoke(delegate
  27.                     {
  28.                         // output is a TextBlock
  29.                         output.Text = reader.ReadToEnd();
  30.                         response.Close();
  31.                     });
  32.                 }));
  33.             }
  34.         }
  35.     }
  36.  
  37.     [DataContract]
  38.     public class Point
  39.     {
  40.         [DataMember]
  41.         public int X { get; set; }
  42.         [DataMember(Name="y")]
  43.         public int Y { get; set; }
  44.     }
  45.  
  46.     public class DataContractQueryStringSerializer
  47.     {
  48.         public void WriteObject(Stream stream, object data)
  49.         {
  50.             StreamWriter writer = new StreamWriter(stream);
  51.             if (data != null)
  52.             {
  53.                 if (data is Dictionary<string, string>)
  54.                 {
  55.                     foreach (var entry in data as Dictionary<string, string>)
  56.                     {
  57.                         writer.Write("{0}={1}&", entry.Key, entry.Value);
  58.                     }
  59.                 }
  60.                 else
  61.                 {
  62.                     foreach (var prop in data.GetType().GetFields())
  63.                     {
  64.                         foreach (var attribute in prop.GetCustomAttributes(true))
  65.                         {
  66.                             if (attribute is DataMemberAttribute)
  67.                             {
  68.                                 DataMemberAttribute member = attribute as DataMemberAttribute;
  69.                                 writer.Write("{0}={1}&", member.Name ?? prop.Name, prop.GetValue(data));
  70.                             }
  71.                         }
  72.                     }
  73.                     foreach (var prop in data.GetType().GetProperties())
  74.                     {
  75.                         if (prop.CanRead)
  76.                         {
  77.                             foreach (var attribute in prop.GetCustomAttributes(true))
  78.                             {
  79.                                 if (attribute is DataMemberAttribute)
  80.                                 {
  81.                                     DataMemberAttribute member = attribute as DataMemberAttribute;
  82.                                     writer.Write("{0}={1}&", member.Name ?? prop.Name, prop.GetValue(data, null));
  83.                                 }
  84.                             }
  85.                         }
  86.                     }
  87.                 }
  88.                 writer.Flush();
  89.             }
  90.         }
  91.     }
  92.  
  93.     public class DataContractMultiPartSerializer
  94.     {
  95.         private string boundary;
  96.         public DataContractMultiPartSerializer(string boundary)
  97.         {
  98.             this.boundary = boundary;
  99.         }
  100.  
  101.         private void WriteEntry(StreamWriter writer, string key, object value)
  102.         {
  103.             if (value != null)
  104.             {
  105.                 writer.Write("--");
  106.                 writer.WriteLine(boundary);
  107.                 if (value is FileInfo)
  108.                 {
  109.  
  110.                     FileInfo f = value as FileInfo;
  111.                     writer.WriteLine(@"Content-Disposition: form-data; name=""{0}""; filename=""{1}""", key, f.Name);
  112.                     writer.WriteLine("Content-Type: application/octet-stream");
  113.                     writer.WriteLine("Content-Length: " + f.Length);
  114.                     writer.WriteLine();
  115.                     writer.Flush();
  116.                     Stream output = writer.BaseStream;
  117.                     Stream input = f.OpenRead();
  118.                     byte[] buffer = new byte[4096];
  119.                     for (int size = input.Read(buffer, 0, buffer.Length); size > 0; size = input.Read(buffer, 0, buffer.Length))
  120.                     {
  121.                         output.Write(buffer, 0, size);
  122.                     }
  123.                     output.Flush();
  124.                     writer.WriteLine();
  125.                 }
  126.                 else
  127.                 {
  128.                     writer.WriteLine(@"Content-Disposition: form-data; name=""{0}""", key);
  129.                     writer.WriteLine();
  130.                     writer.WriteLine(value.ToString());
  131.                 }
  132.             }
  133.         }
  134.  
  135.         public void WriteObject(Stream stream, object data)
  136.         {
  137.             StreamWriter writer = new StreamWriter(stream);
  138.             if (data != null)
  139.             {
  140.                 if (data is Dictionary<string, object>)
  141.                 {
  142.                     foreach (var entry in data as Dictionary<string, object>)
  143.                     {
  144.                         WriteEntry(writer, entry.Key, entry.Value);
  145.                     }
  146.                 }
  147.                 else
  148.                 {
  149.                     foreach (var prop in data.GetType().GetFields())
  150.                     {
  151.                         foreach (var attribute in prop.GetCustomAttributes(true))
  152.                         {
  153.                             if (attribute is DataMemberAttribute)
  154.                             {
  155.                                 DataMemberAttribute member = attribute as DataMemberAttribute;
  156.                                 WriteEntry(writer, member.Name ?? prop.Name, prop.GetValue(data));
  157.                             }
  158.                         }
  159.                     }
  160.                     foreach (var prop in data.GetType().GetProperties())
  161.                     {
  162.                         if (prop.CanRead)
  163.                         {
  164.                             foreach (var attribute in prop.GetCustomAttributes(true))
  165.                             {
  166.                                 if (attribute is DataMemberAttribute)
  167.                                 {
  168.                                     DataMemberAttribute member = attribute as DataMemberAttribute;
  169.                                     WriteEntry(writer, member.Name ?? prop.Name, prop.GetValue(data, null));
  170.                                 }
  171.                             }
  172.                         }
  173.                     }
  174.                 }
  175.             }
  176.             writer.Write("--");
  177.             writer.Write(boundary);
  178.             writer.WriteLine("--");
  179.             writer.Flush();
  180.         }
  181.     }
  182.  
  183.     public static class Extensions
  184.     {
  185.         public static void PostFormAsync(this HttpWebRequest request, object parameters, AsyncCallback callback)
  186.         {
  187.             request.Method = "POST";
  188.             request.ContentType = "application/x-www-form-urlencoded";
  189.             request.BeginGetRequestStream(new AsyncCallback(asyncResult =>
  190.             {
  191.                 Stream stream = request.EndGetRequestStream(asyncResult);
  192.                 DataContractQueryStringSerializer ser = new DataContractQueryStringSerializer();
  193.                 ser.WriteObject(stream, parameters);
  194.                 stream.Close();
  195.                 request.BeginGetResponse(callback, request);
  196.             }), request);
  197.         }
  198.  
  199.         public static void PostMultiPartAsync(this HttpWebRequest request, object parameters, AsyncCallback callback)
  200.         {
  201.             request.Method = "POST";
  202.             string boundary = "---------------" + DateTime.Now.Ticks.ToString();
  203.             request.ContentType = "multipart/form-data; boundary=" + boundary;
  204.             request.BeginGetRequestStream(new AsyncCallback(asyncResult =>
  205.             {
  206.                 Stream stream = request.EndGetRequestStream(asyncResult);
  207.  
  208.                 DataContractMultiPartSerializer ser = new DataContractMultiPartSerializer(boundary);
  209.                 ser.WriteObject(stream, parameters);
  210.                 stream.Close();
  211.                 request.BeginGetResponse(callback, request);
  212.             }), request);
  213.         }
  214.     }
  215. }