Invoking a Partial Update from Flash
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():
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:
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.
Coming Soon: Facebook Controls for ASP.NET
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:
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:
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.
Creating a Sniffer for ASP.NET
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.
