Running with Code Like with scissors, only more dangerous

17Mar/090

Invoking a Partial Update from Flash

Posted by Rob

This morning I received an email that posed a question so interesting that I thought I would blog about the answer.  The question was, essentially, how can we invoke a partial update (using ASP.NET AJAX triggers), from an on(up) button handler in Flash?

There are a few different ways to approach this problem.  I believe the method I’m going to write out here is what I like to call the “path of least resistance” – it’ll get you there quickly.  However, it will create some interdependencies among your controls.  However, using this technique as a baseline, you should be able to adapt it to fit other better techniques, which I’ll describe later.

Flash lives in a sandbox relative to your web page; it doesn’t interact with the rest of your page’s code (by default), and so you need to provide it with a way to do so.  In ActionScript 1 and 2 it’s relatively easy to invoke JavaScript using getURL():

Invoking a JavaScript call

I won’t get into the details of how to access script with ActionScript; I’ll simply leave it to the professionals (and use this as an example).  With this capability we should have everything we need to build an AJAX-enabled Flash button.  I’ve created a sample ASPX page that will host what we need:

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="150" height="60" id="blog" align="middle">
            <param name="allowScriptAccess" value="always" />
            <param name="allowFullScreen" value="false" />
            <param name="movie" value="blog.swf" /><param name="quality" value="high" /><param name="bgcolor" value="#ffffff" />    
            <embed src="blog.swf" quality="high" bgcolor="#ffffff" width="150" height="60" name="blog" align="middle" allowScriptAccess="always" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" />
        </object>
    <asp:ScriptManager ID="sm" runat="server" EnablePartialRendering="true">
        
    </asp:ScriptManager>
    
    <asp:UpdatePanel ID="update" runat="server">
        <Triggers>
            
        </Triggers>
        <ContentTemplate>
            <asp:Label ID="lblText" runat="server" Text="Click the Flash button to turn me blue." />
        </ContentTemplate>
    </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>

We still need a way to invoke the partial update.  Unfortunately, what I termed the “path of least resistance” is going to involve a little voodoo: we’re going to create a Button, set its display to none (so that it’s on the page but hidden), and then treat it as a trigger for the UpdatePanel:

        <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="150" height="60" id="blog" align="middle">
            <param name="allowScriptAccess" value="always" />
            <param name="allowFullScreen" value="false" />
            <param name="movie" value="blog.swf" /><param name="quality" value="high" /><param name="bgcolor" value="#ffffff" />    
            <embed src="blog.swf" quality="high" bgcolor="#ffffff" width="150" height="60" name="blog" align="middle" allowScriptAccess="sameDomain" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" />
        </object>
        <asp:Button runat="server" style="display: none;" ID="btnFlashGo" />
    <asp:ScriptManager ID="sm" runat="server" EnablePartialRendering="true">
        
    </asp:ScriptManager>
    
    <asp:UpdatePanel ID="update" runat="server">
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="btnFlashGo" EventName="Click" />
        </Triggers>
        <ContentTemplate>
            <asp:Label ID="lblText" runat="server" Text="Click the Flash button to turn me blue." />
        </ContentTemplate>
    </asp:UpdatePanel>

We’re still missing one piece: we need to generate the function call to actually make a postback.  There’s a relatively convenient way to do that, using the ClientScriptManager; drop this Literal onto the page:

        <asp:Button runat="server" style="display: none;" ID="btnFlashGo" OnClick="btnFlashGo_Click" />
        <asp:Literal ID="flashScript" runat="server" />

and wire it up in the backend:

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.flashScript.Text = string.Format(@"<script type=""text/javascript"">
function flashClicked()
{{
    {0}
}}
</script>", ClientScript.GetPostBackEventReference(this.btnFlashGo, ""));
    }

    protected void btnFlashGo_Click(object sender, EventArgs e)
    {
        lblText.ForeColor = Color.Blue;
    }
}

This setup should give us everything we need to make the AJAX call, and sure enough:

image

Better Approaches

I would be happier – much happier – with this solution if it didn’t depend on so much black magic.  There are a few ways to get it to work better, and while I don’t want to take the time to demonstrate them here, I can describe them a bit.

A FlashButton Control

In my mind, a FlashButton control is the best solution; it can derive from Control or WebControl (although to be honest simply Control is preferable), and can automatically do the heavy lifting.  You could incorporate SWFObject as a script resource and provide it automatically.  FlashButton could expose its own events, which means that you could eliminate that Button (the hidden one) and instead create the script callback reference pointing to the FlashButton’s event itself (and the UpdatePanel’s trigger could point there as well). 

A FlashButtonManager Control

A FlashButtonManager could extend the support for FlashButton much like ScriptManager does for UpdatePanel.  While the approach with a single FlashButton works well when only a single FlashButton is on the page, incorporating multiple FlashButton objects becomes tricky when you factor in things like handling multiple callbacks (for instance, naming the flashClicked() function).  A FlashButtonManager could be designed such that it handles each flashButton on the page, perhaps setting up FlashButtons with a FlashVar to specify a parameter when calling flashClicked(), and then using that parameter to determine which one was clicked and firing the appropriate postback event.

Final Thoughts

You need to enable your Flash app to talk to JavaScript in order to make AJAX partial updates work correctly.  Fortunately it’s not super-challenging to do so!  You should be careful though – there is some voodoo going on in the back-end of this kind of solution – but with creative architecture, you can avoid a lot of headache.

25Feb/090

Coming Soon: Facebook Controls for ASP.NET

Posted by Rob

On March 10th, I’ll be presenting an ASP.NET control library for Facebook at the Phoenix ASP.NET Users Group.  Among other things, we’ll be talking about how to create a Facebook application using ASP.NET, debugging aids, and the Facebook API.  Along with that, Terralever will be releasing our library to the open-source community on Codeplex.  (This post will be updated once we’ve done so).

The library is currently in varying degrees of maturity.  We’ve got about half of the FBML controls supported in the library with full design-time support:

An fb:is-in-network control displaying its error state.

If you haven’t worked with Facebook before, aside from having design-time support, the best part about having Facebook controls with runat=”server” on them is that you have full access to them in codebehind.  Because of the way Facebook controls are accessed (using the XML fb: prefix), we couldn’t do that before without causing exceptions because ASP.NET would try to access them.  That generally led us to using ugly ASP.NET databinding syntax:

<fb:name usereflexive=”true” useyou=”true” capitalize=”true” uid=’<%# Eval(“FbUserID”) %>’ />

We still have the ability to use this type of syntax, but we have the flexibility to do specific databinding (for instance, within a repeater control), accessing that control within a type-safe way.

There are a few big differences, though.  Facebook has this concept of a meta-control called fb:else; it’s used in conjunction with several other controls (such as the one shown in the screenshot above).  But that’s DEFINITELY not supported in the ASP.NET code editor, and it’s fairly irregular for an ASP.NET application.  Fortunately, we already have a way around that problem, templates:

The fb:is-in-network source code.

By using designer templates we’re able to provide that metadata to the editor.  (More on that before too long).

Release Roadmap

For the release in early March following the user group presentation, we’ll release a subset of the FBML user controls (the Terralever.Facebook.UI.FbmlControls namespace) to the community, as well as some base-level utility classes, such as base classes for Canvas- and IFrame-based pages.  Hopefully, there will also be a preview of LINQ-to-FQL, although that’s not necessarily going to happen.  Debugging aids, including specialized and extended tracing HTTP modules, will be part of the release.  We’ll also include a couple controls we’ve had to model, including a Pager control, a Birthday List control, and a control template for hosting on user profile boxes.

Version 1

Additional FBML controls will be added as time goes on, but the second biggest release is going to be adding support to the Terralever.Facebook.UI.CanvasControls namespace.  Just like the System.Web.UI.WebControls namespace is to the System.Web.UI.HtmlControls namespace, .CanvasControls is to .FbmlControls.  It provides support for some of the higher-level functionality that we’ve come to expect from ASP.NET.  If you’ve tried to use the normal validation controls on a Facebook Canvas page, you’ve known the bitter defeat of it.  I’m not so sure it’ll get to some of the more data-centric controls we’ve come to know and love, like DataGridView (because let’s be honest – it just doesn’t fit into a Facebook page).  But we’ll see some support for postbacks (on that note, Microsoft, ClientScriptManager shouldn’t be sealed and Page.ClientScript should be virtual), all the regular validator controls, and some Facebook-friendly client UI controls. 

We’ll include much greater support for the Facebook REST-based API.  I haven’t decided yet whether we’ll consume them as WCF services or simply use an XML parsing strategy, such as LINQ-to-XML.  It it isn’t already included, we’ll definitely include a LINQ-to-FQL implementation.

Version 2

Long-term (version 2 and beyond) will most likely add support for the Facebook internationalization platform, the Editor control, and the Mobile platform.  We’ll see how it goes and what is requested by the community.

13May/080

Creating a Sniffer for ASP.NET

Posted by Rob

This afternoon, some co-workers were a bit perplexed about why an 'ñ' character in a Spanish name wasn't displaying on a Facebook application we were working on, but rather was coming across as a '?'.  Because we were pretty sure that Facebook was UTF-8-encoded, we thought the problem was on our side.  Unfortunately, we were on a client's staging/test server, and we weren't able to debug on that machine (we have Visual Studio installed on our own test server).  Since we didn't have privileges on the server, we couldn't even jump on WireShark to validate the outgoing packets from our server.  We needed an alternative method.

One of the cool things about HTTP Modules in ASP.NET is that they sit between the client and the server; with them, we can get all the information we need.  In addition, we can pass the output through a filter - a Stream object through which the page output passes.  This is exposed through the HttpResponse.Filter property.

The solution to this problem is straightforward: we'll inject a filter into the response stream and write all of the output to a log file.  Then, we'll be able to see whether we're sending the offending ?, or if Facebook is misreading our character and translating it to a ?.

First, I created the HTTP Module:

   1: using System;
   2: using System.Web;
   3:  
   4: public class SnifferHttpModule : IHttpModule
   5: {
   6:     #region IHttpModule Members
   7:  
   8:     public void Dispose()
   9:     {
  10:         
  11:     }
  12:  
  13:     public void Init(HttpApplication context)
  14:     {
  15:         context.BeginRequest += new EventHandler(context_BeginRequest);
  16:     }
  17:  
  18:     void context_BeginRequest(object sender, EventArgs e)
  19:     {
  20:         HttpContext.Current.Response.Filter = GetFilterFor(HttpContext.Current);
  21:     }
  22:     #endregion
  23:  
  24:     private static System.IO.Stream GetFilterFor(HttpContext httpContext)
  25:     {
  26:         return new LogFilter(httpContext, httpContext.Response.Filter);
  27:     }
  28: }

Then we had to implement an actual filter stream object:

   1: using System;
   2: using System.Web;
   3: using System.IO;
   4: using MBNCSUtil;
   5: using System.Text;
   6:  
   7: /// <summary>
   8: /// Summary description for LogFilter
   9: /// </summary>
  10: public class LogFilter : Stream
  11: {
  12:     private Stream _logStream, _sink, _binaryStream;
  13:     private StreamWriter _binaryWriter;
  14:     public LogFilter(HttpContext context, Stream baseStream)
  15:     {
  16:         _logStream = File.OpenWrite(
  17:             Path.Combine(
  18:                 context.Server.MapPath("~/logs"),
  19:                 string.Concat(DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss"), ".txt")
  20:                 )
  21:             );
  22:  
  23:         WriteHeadersToLog(_logStream, "Request Headers", context.Request.Headers);
  24:         
  25:         _binaryStream = File.OpenWrite(
  26:             Path.Combine(
  27:                 context.Server.MapPath("~/logs"),
  28:                 string.Concat(DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss"), "-binary.txt")
  29:                 )
  30:             );
  31:         _binaryWriter = new StreamWriter(_binaryStream, Encoding.UTF8);
  32:         _sink = baseStream;
  33:     }
  34:  
  35:     private static void WriteHeadersToLog(Stream logStream, string title, System.Collections.Specialized.NameValueCollection nameValueCollection)
  36:     {
  37:         StreamWriter sw = new StreamWriter(logStream);
  38:         sw.WriteLine(title);
  39:         sw.WriteLine();
  40:         foreach (string name in nameValueCollection.AllKeys)
  41:         {
  42:             string value = nameValueCollection[name];
  43:             if (value == null)
  44:                 value = "(nil)";
  45:             else if (value.Length == 0)
  46:                 value = "(empty string)";
  47:  
  48:             sw.WriteLine("{0}: {1}", name, value);
  49:         }
  50:         sw.WriteLine();
  51:         sw.Flush();
  52:     }
  53:  
  54:     public override bool CanRead
  55:     {
  56:         get { return false; }
  57:     }
  58:  
  59:     public override bool CanSeek
  60:     {
  61:         get { return false; }
  62:     }
  63:  
  64:     public override bool CanWrite
  65:     {
  66:         get { return true; }
  67:     }
  68:  
  69:     public override void Flush()
  70:     {
  71:         _logStream.Flush();
  72:         _sink.Flush();
  73:         _binaryStream.Flush();
  74:     }
  75:  
  76:     public override long Length
  77:     {
  78:         get { return _sink.Length; }
  79:     }
  80:  
  81:     public override long Position
  82:     {
  83:         get
  84:         {
  85:             return _sink.Length;
  86:         }
  87:         set
  88:         {
  89:             throw new InvalidOperationException();
  90:         }
  91:     }
  92:  
  93:     public override int Read(byte[] buffer, int offset, int count)
  94:     {
  95:         throw new InvalidOperationException();
  96:     }
  97:  
  98:     public override long Seek(long offset, SeekOrigin origin)
  99:     {
 100:         throw new InvalidOperationException();
 101:     }
 102:  
 103:     public override void SetLength(long value)
 104:     {
 105:         _sink.SetLength(value);
 106:     }
 107:  
 108:     public override void Write(byte[] buffer, int offset, int count)
 109:     {
 110:         _sink.Write(buffer, offset, count);
 111:         _logStream.Write(buffer, offset, count);
 112:         string text = DataFormatter.Format(buffer);
 113:         _binaryWriter.WriteLine(text);
 114:     }
 115:  
 116:     public override void Close()
 117:     {
 118:         base.Close();
 119:         _logStream.Close();
 120:         _sink.Close();
 121:         _binaryWriter.Close();
 122:         _binaryStream.Close();
 123:     }
 124: }

Notable in this class --

  • It's not readable or seekable. 
  • It creates two log files per transaction: a dump of the request headers and the response page, and a dump in hex and binary.  The hex-and-binary dump uses the DataFormatter class (username: 'mbncsutil_anonymous', no password) of the MBNCSUtil library, which I won't include here.
  • The Write, Flush, and Close operations are overridden to perform those operations on both of the logs and the contained stream.
  • The existing Response.Filter object is passed as a constructor to this new filter, and is contained as the member field _sink.  If the existing Response.Filter is not used, an exception is raised, because it means that the output isn't getting written to the stream that will eventually be sent to the client.
  • I implemented a helper method called WriteHeadersToLog because I wanted to dump both request and response headers to the log file.  However, I was rudely informed by ASP.NET that it would require IIS7 Integrated Pipeline mode.  So I removed it.

There are some nice files produced.  Here's an example of one:

   1: Request Headers
   2:  
   3: Connection: keep-alive
   4: Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
   5: Accept-Encoding: gzip, deflate
   6: Accept-Language: en-US
   7: Host: localhost:62905
   8: User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15
   9:  
  10:  
  11:  
  12: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  13:  
  14: <html xmlns="http://www.w3.org/1999/xhtml">
  15: <head><title>
  16:     Untitled Page
  17: </title></head>
  18: <body>
  19:     <form name="form1" method="post" action="Default.aspx" id="form1">
  20: <div>
  21: <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUJNzgzNDMwNTMzZGRurqWwMv9+8ZxlGC8bdg4AJen5vA==" />
  22: </div>
  23:  
  24:     <div>
  25:         This is a test.
  26:     </div>
  27:     </form>
  28: </body>
  29: </html>

And here's an example of the binary:

0000   0d 0a 0d 0a 3c 21 44 4f  43 54 59 50 45 20 68 74    ....<!DOCTYPE ht

0010   6d 6c 20 50 55 42 4c 49  43 20 22 2d 2f 2f 57 33    ml PUBLIC "-//W3

0020   43 2f 2f 44 54 44 20 58  48 54 4d 4c 20 31 2e 30    C//DTD XHTML 1.0

0030   20 54 72 61 6e 73 69 74  69 6f 6e 61 6c 2f 2f 45     Transitional//E

0040   4e 22 20 22 68 74 74 70  3a 2f 2f 77 77 77 2e 77    N" "http://www.w

0050   33 2e 6f 72 67 2f 54 52  2f 78 68 74 6d 6c 31 2f    3.org/TR/xhtml1/

0060   44 54 44 2f 78 68 74 6d  6c 31 2d 74 72 61 6e 73    DTD/xhtml1-trans

0070   69 74 69 6f 6e 61 6c 2e  64 74 64 22 3e 0d 0a 0d    itional.dtd">...

0080   0a 3c 68 74 6d 6c 20 78  6d 6c 6e 73 3d 22 68 74    .<html xmlns="ht

0090   74 70 3a 2f 2f 77 77 77  2e 77 33 2e 6f 72 67 2f    tp://www.w3.org/

00a0   31 39 39 39 2f 78 68 74  6d 6c 22 3e 0d 0a 3c 68    1999/xhtml">..<h

00b0   65 61 64 3e 3c 74 69 74  6c 65 3e 0d 0a 09 55 6e    ead><title>...Un

00c0   74 69 74 6c 65 64 20 50  61 67 65 0d 0a 3c 2f 74    titled Page..</t

00d0   69 74 6c 65 3e 3c 2f 68  65 61 64 3e 0d 0a 3c 62    itle></head>..<b

00e0   6f 64 79 3e 0d 0a 20 20  20 20 3c 66 6f 72 6d 20    ody>..    <form

00f0   6e 61 6d 65 3d 22 66 6f  72 6d 31 22 20 6d 65 74    name="form1" met

0100   68 6f 64 3d 22 70 6f 73  74 22 20 61 63 74 69 6f    hod="post" actio

0110   6e 3d 22 44 65 66 61 75  6c 74 2e 61 73 70 78 22    n="Default.aspx"

0120   20 69 64 3d 22 66 6f 72  6d 31 22 3e 0d 0a 3c 64     id="form1">..<d

0130   69 76 3e 0d 0a 3c 69 6e  70 75 74 20 74 79 70 65    iv>..<input type

0140   3d 22 68 69 64 64 65 6e  22 20 6e 61 6d 65 3d 22    ="hidden" name="

0150   5f 5f 56 49 45 57 53 54  41 54 45 22 20 69 64 3d    __VIEWSTATE" id=

0160   22 5f 5f 56 49 45 57 53  54 41 54 45 22 20 76 61    "__VIEWSTATE" va

0170   6c 75 65 3d 22 2f 77 45  50 44 77 55 4a 4e 7a 67    lue="/wEPDwUJNzg

0180   7a 4e 44 4d 77 4e 54 4d  7a 5a 47 52 75 72 71 57    zNDMwNTMzZGRurqW

0190   77 4d 76 39 2b 38 5a 78  6c 47 43 38 62 64 67 34    wMv9+8ZxlGC8bdg4

01a0   41 4a 65 6e 35 76 41 3d  3d 22 20 2f 3e 0d 0a 3c    AJen5vA==" />..<

01b0   2f 64 69 76 3e 0d 0a 0d  0a 20 20 20 20 3c 64 69    /div>....    <di

01c0   76 3e 0d 0a 20 20 20 20  20 20 20 20 54 68 69 73    v>..        This

01d0   20 69 73 20 61 20 74 65  73 74 2e 0d 0a 20 20 20     is a test...  
01e0   20 3c 2f 64 69 76 3e 0d  0a 20 20 20 20 3c 2f 66     </div>..    </f

01f0   6f 72 6d 3e 0d 0a 3c 2f  62 6f 64 79 3e 0d 0a 3c    orm>..</body>..<

0200   2f 68 74 6d 6c 3e 0d 0a                              /html>..

Some notes about using this code:

  • It's not meant to be used in production code.  It doesn't even attempt to synchronize file I/O.
  • It *can* be used as a starter if, for instance, you wanted to compress HTTP traffic.  (This is the same method the Blowery compression module takes).
  • You need to add an HTTP Module declaration to your web.config (see below).
  • Facebook is difficult to debug.

Setting up your Web.config file

Remember that you need to add the <httpModules> section to your web.config file.  If you don't already have one, this will suffice:

   1: <httpModules>
   2:     <add name="Filter" type="SnifferHttpModule, __code"/>
   3: </httpModules>

This assumes that the module is being added from App_Code.

Complete source code can be downloaded here.  Totally free, except for DataFormatter - that's licensed under a BSD-ish license. :-)