Wednesday, October 28, 2009

Exporting data from Silverlight

We can put a lot of time and effort figuring out what our customers want to do with data, and tailoring the UI towards making those tasks pleasant, efficient, and seamless. But there comes a point where the user wants to put the data into a spreadsheet and analyze/share from there. Or, they may just want to print the data.

Silverlight datagrids are not very helpful when it comes to exporting data. And, printer support for silverlight is non-existent. We got around this limitation by using a WebHandler that receives the export request, and returns a csv file.

The export request contains all the information about the service method and parameters that populated data in the datagrid.

The ExportRequestData is serialized by the silverlight client, passed to the handler on the query string, and deserialized by the handler. So, both the web and silverlight projects needs to understand this class. You can do this by creating a separate dll and referencing it in both the projects, but we were in tactical mode at the time and put a file in one project and just made a link to it in the other. Just make sure you don't introduce any code in it that Silverlight doesn't understand (since SL is a subset of full-fledged .net).

Here is the code for ExportRequestData class:



   1:  //This file is owned by the web project and linked to the silveright project.
   2:  //It is safer to edit this file within the context of silverlight
   3:  //because then the intellisense only shows the methods and properties you can use within Silverlight
   4:  //framework. If edited within web context, intellisense will show stuff you can do within
   5:  //full-fledged .NET, and if you end up using stuff outside of silverlight framework, the silverlight
   6:  //app will not build.
   7:   
   8:  public class ExportRequestData
   9:  {
  10:      private Dictionary<string, string> _Parameters;
  11:      public Dictionary<string, string> Parameters
  12:      {
  13:          get
  14:          {
  15:              if (_Parameters == null)
  16:                  _Parameters = new Dictionary<string, string>();
  17:              return _Parameters;
  18:          }
  19:          set { _Parameters = value; }
  20:      }
  21:      public string Method { get; set; }
  22:   
  23:      public void AddParam(string key, object value)
  24:      {
  25:          if (value != null)
  26:          {
  27:              Parameters.Add(key, value.ToString());
  28:          }
  29:      }
  30:   
  31:      //expected format : method_name;param1=value1|param2=value2|param3=value3
  32:      public string Serialize()
  33:      {
  34:          StringBuilder sb = new StringBuilder();
  35:          sb.Append(Method.ToString()).Append(";");
  36:          foreach (KeyValuePair<string, string> kv in Parameters)
  37:          {
  38:              sb.Append(kv.Key).Append("=").Append(kv.Value).Append("|");
  39:          }
  40:          string ret = sb.ToString();
  41:          return ret.Remove(ret.Length - 1);
  42:      }
  43:   
  44:   
  45:      //expected format : method_name;param1=value1|param2=value2|param3=value3
  46:      public static ExportRequestData Deserialize(string data)
  47:      {
  48:          ExportRequestData d = new ExportRequestData();
  49:          d.Parameters = new Dictionary<string, string>();
  50:   
  51:          string[] splitData = data.Split(new char[] { ';' });
  52:          d.SetMethod(splitData[0]);
  53:          if (splitData.Length > 1)
  54:          {
  55:              string[] p = splitData[1].Split(new char[] { '|' });
  56:              char[] paramSplitter = new char[] { '=' };
  57:              foreach (string s in p)
  58:              {
  59:                  string[] param = s.Split(paramSplitter);
  60:                  if (param.Length > 1)
  61:                      d.Parameters.Add(param[0], param[1]);
  62:              }
  63:          }
  64:          return d;
  65:      }
  66:   
  67:      public string GetParam(string key)
  68:      {
  69:         
  70:          if (Parameters.ContainsKey(key))
  71:              return Parameters[key];
  72:          else return null;
  73:      }
  74:   
  75:      public bool? GetBoolParam(string key)
  76:      {
  77:          object ret = GetParam(key);
  78:          if (ret != null)
  79:          {
  80:              return Convert.ToBoolean(ret);
  81:          }
  82:          else return null;
  83:      }
  84:   
  85:   
  86:      public short? GetShortParam(string key)
  87:      {
  88:          object ret = GetParam(key);
  89:          if (ret != null)
  90:          {
  91:              return Convert.ToInt16(ret);
  92:          }
  93:          else return null;
  94:      }
  95:   
  96:      public byte? GetByteParam(string key)
  97:      {
  98:          object ret = GetParam(key);
  99:          if (ret != null)
 100:          {
 101:              return Convert.ToByte(ret);
 102:          }
 103:          else return null;
 104:      }
 105:   
 106:      public int? GetIntParam(string key)
 107:      {
 108:          object ret = GetParam(key);
 109:          if (ret != null)
 110:          {
 111:              return Convert.ToInt32(ret);
 112:          }
 113:          else return null;
 114:      }
 115:   
 116:      public DateTime? GetDateTimeParam(string key)
 117:      {
 118:          object ret = GetParam(key);
 119:          if (ret != null)
 120:          {
 121:              return Convert.ToDateTime(ret);
 122:          }
 123:          else return null;
 124:      }
 125:   
 126:      public double? GetDoubleParam(string key)
 127:      {
 128:          object ret = GetParam(key);
 129:          if (ret != null)
 130:          {
 131:              return Convert.ToDouble(ret);
 132:          }
 133:          else return null;
 134:      }
 135:  }

The client tacks on the serialized ExportRequestData object (handler of Click event of the "Export" HyperlinkButton):



   1:                  var rd = new ExportRequestData { Method = "MyMethod" };
   2:                  rd.AddParam("FirstParam", param1Value);
   3:                  rd.AddParam("SecondParam", param2Value);
   4:                  rd.AddParam("ThirdParam", param3Value);
   5:                  MyHyperLinkButton.NavigateUri = new Uri("http://mydomain.com/handler.ashx?data=" HttpUtility.UrlEncode(data.Serialize()));

You will probably add authentication and other things to this. The caveat here is that you have to match the method and parameter names exactly to the ones in the web service. The order in which they are added, doesn't matter. Now, we are all set with the client. Onwards to the server code.

The handler takes the request, deserializes it, calls the method with the parameters described in the request, and outputs the results as a csv.

So the ProcessRequest in the handler looks like this:



   1:  //Re-construct the ExportRequestData object:
   2:  ExportRequestData rd = ExportRequestData.Deserialize(context.Request.QueryString["data"].ToString());
   3:  ExportResults(rd);

ExportResults method uses reflection call the right method and get results. It casts the results to Object[], which then gets passed to a generic CsvExporter.



   1:      private void ExportResults(ExportRequestData rd)
   2:      {
   3:          MyWebService svc = new MyWebService();
   4:          MethodInfo mi = typeof(MyWebService).GetMethod(rd.Method);
   5:          ParameterInfo[] pis = mi.GetParameters();
   6:          object[] ps = new object[pis.Length];
   7:          for (int i = 0; i < pis.Length; i++)
   8:          {
   9:              if (pis[i].ParameterType == typeof(string))
  10:                  ps[i] = rd.GetParam(pis[i].Name);
  11:              else if (pis[i].ParameterType == typeof(DateTime?))
  12:                  ps[i] = rd.GetDateTimeParam(pis[i].Name);
  13:              else if (pis[i].ParameterType == typeof(short?))
  14:                  ps[i] = rd.GetShortParam(pis[i].Name);
  15:              else if (pis[i].ParameterType == typeof(int?))
  16:                  ps[i] = rd.GetIntParam(pis[i].Name);
  17:              else if (pis[i].ParameterType == typeof(double?))
  18:                  ps[i] = rd.GetDoubleParam(pis[i].Name);
  19:              else if (pis[i].ParameterType == typeof(byte?))
  20:                  ps[i] = rd.GetByteParam(pis[i].Name);
  21:              else if (pis[i].ParameterType == typeof(bool?))
  22:                  ps[i] = rd.GetBoolParam(pis[i].Name);
  23:          }
  24:          CsvExporter.Export((Object[])mi.Invoke(svc, ps), rd.Method, true);
  25:      }

The CsvExporter examines the actual type of the object in Object[] passed to it (it assumes that all the objects in the array are of the same type). It uses this info to create the header. In addition, if a property of the object is numeric, it adds a "=" in front of the value so it shows up correctly when displayed in Excel. Here is the code for the Exporter:



   1:   
   2:      public static Regex NumericRegex;
   3:   
   4:      static CsvExporter()
   5:      {
   6:          NumericRegex = new Regex(@"^\d+$");
   7:      }
   8:     
   9:      public static void Export(Object[] source, string fileName)
  10:      {
  11:          HttpResponse response = HttpContext.Current.Response;
  12:          response.ContentType = "text/plain";
  13:          response.AddHeader("Content-Disposition", "attachment;filename=\"" + fileName + "_" + DateTime.Now.ToString("MMddyyHHmmss") + ".csv\"");
  14:          response.Clear();
  15:   
  16:          StreamWriter csvDoc = new StreamWriter(response.OutputStream);
  17:          if (source.Length > 0)
  18:          {
  19:              PropertyInfo[] props = source[0].GetType().GetProperties();
  20:              foreach (PropertyInfo prop in props)
  21:              {
  22:                  csvDoc.Write("\"" + prop.Name.Replace("_", " ") + "\",");
  23:              }
  24:              csvDoc.WriteLine();
  25:              foreach (object o in source)
  26:              {
  27:                  foreach (PropertyInfo prop in props)
  28:                  {
  29:                      object v = prop.GetValue(o, null);
  30:                      if (v != null && (prop.PropertyType == typeof(string)) && NumericRegex.IsMatch(v.ToString()))
  31:                      {
  32:                          csvDoc.Write("=");
  33:                      }
  34:                      csvDoc.Write("\"");
  35:                      csvDoc.Write(v);
  36:                      csvDoc.Write("\",");
  37:                  }
  38:                  csvDoc.WriteLine();
  39:              }
  40:          }
  41:          csvDoc.Flush();
  42:          csvDoc.Close();
  43:          response.End();
  44:      }

With this, we were able to implement a generic and reusable system to export data from our Silverlight UIs. I hope the info is helpful.

No comments:

Post a Comment