CSharp InLine Publisher
From Facebook Developer Wiki
Contents |
Introduction
When a user has added your app they can see a link to your app in the Publisher on their own wall or on their friend's wall. When they click this link button FB makes a call to one of 2 URLs specified in your application settings: one URL for clicks originating on your own wall, and the second URL for clicks from another's wall.
This post will demonstrate how to handle posts from your own wall. This code can easily be modified to handle the other case as the parameters and methods are all the same, only the content can be different. If your app does not need to differentiate between the two activity sources, you can use the same URL for both properties.
The process flows as follows:
Providing the Publisher Interface: 1. User clicks on app link 2. FB posts to the callback URL, sending method="publisher_getInterface" 3. Your app returns a JSON block specifying the interface 4. FB renders this JSON to create the UI. Publishing the Feed Story: 1. The user interacts with your UI, eventually clicking "Submit" 2. FB posts to the callback URL sending method="publisher_getFeedStory" 3. Your app returns a JSON block specifying a Feed Template to be published.
Providing the Publisher Interface
Unlike a normal .NET page request, FB is not expecting a web page in response. It is expecting a very specific block of JSON text, and any deviation from it will result in an error. The JSON result format is:
{"content":
{"fbml" : "any FBML markup text" ,
"publishEnabled" : true,
"commentEnabled" : true
} ,
"method" : "publisher_getInterface"
}
- An unnamed top-level element containing two members: "content" and "method".
- "content" contains 3 elements named "fbml", "publishEnabled", and "commentEnabled".
- Element "fbml" contains the entire UI FBML markup for the publisher inline interface.
- "publishEnabled" indicates the default state of the "Submit" button.
- "commentEnabled" controls the user's ability to enter a text comment.
- "method" indicates what function FB is invoking.
At this point, FB is requesting the publisher interface, so method="publisher_getInterface". In the second half of this lifecycle, the method will be a value that indicates FB is requesting the Feed Story details.
The FBML markup is the tricky piece here. For example, a basic ListView with Object Data Source... 5 minutes in a standard .NET web site. But, this FB response format requires the client-side markup fully rendered, and escaped correctly to be JSON compatible. Luckily the FDT has some features to make this a bit easier.
The Web User Control
I start by building my UI as a Web User Control. This code includes CSS and javascript that allow for selection of one of the displayed items, and passing that selected ID back with the second postback. More about that in the second half. This is really a very simple WUC...
wucMyGrid.ascx
<%@ Control Language="C#" AutoEventWireup="true"
CodeFile="wucMyGrid.ascx.cs" Inherits="wucMyGrid" %>
<style type="text/css">
.it { margin-left:-40px; }
.ListItem {display:inline; float:left; border-color:White; border-style:solid;
border-width:1px; margin:1px;}
.selectedListItem { display:inline; float:left; border-color:Red;
border-style:solid; border-width:1px; margin:1px; }
</style>
<script type="text/javascript">
var selectedID;
function setSelected(selectedDiv) {
if (selectedID != null) {
var oldDiv = document.getElementById(selectedID);
oldDiv.removeClassName("selectedListItem");
oldDiv.addClassName("ListItem");
}
selectedDiv.removeClassName("ListItem");
selectedDiv.addClassName("selectedListItem");
selectedID = selectedDiv.getId();
var p = document.getElementById("picked");
if (p != null) {p.setValue(selectedID);}
}
</script>
<div class="it">
<asp:ListView ID="ListView1" runat="server">
<LayoutTemplate>
<div style="overflow-y:scroll; overflow-x:hidden; height:300px; width:100%">
<ul >
<asp:Placeholder id="itemPlaceholder" runat="server" />
</ul>
</div>
</LayoutTemplate>
<ItemTemplate>
<li class="ListItem">
<div class="ListItem" id="<%# Eval("ThingID") %>"
onclick="setSelected(this);return false;" >
<img alt="<%# Eval("Title") %>" src="<%# Eval("ImageFileName") %>"
height="80px" width="80px"/>
</div>
</li>
</ItemTemplate>
</asp:ListView>
</div>
and the code-behind
wucMyGrid.ascx.cs
using System.Collections;
using facebook.web;
public partial class wucMyGrid : System.Web.UI.UserControl, IRenderableFBML<List<Thing>>
{
List<Thing> things;
public void PopulateData(List<Thing> _things)
{
things = _things;
ListView1.DataSource = things;
ListView1.DataBind();
}
}
The Listview control renders the items from my object data source using the specified templates. The CSS styles control their position and appearance. Notice that the control's class implements IRenderableFBML<List<Thing>> ("Thing" is a business object class, the publisher is presenting a list of "Things" to the user.) The IRenderableFBML interface requires that a method PopulateData is implemented. This method is invoked in the flow of the control rendering in the publisher callback handler, allowing for dynamic population of data on the inline publisher interface. In this example, we have defined the data type as a List of Thing objects, and bound it to the Listview.
User Input, Hidden Fields added late
The Listview templates define an on-click event for each Thing's entry in the listview. This event invokes a javascript method that changes the style on the clicked div, and records the div ID in a hidden field. The hidden field is however NOT included in the markup of the web user control, it is in fact added later, after the markup text is generated. I am not sure why it works that way, however I found it necessary in order to retrieve the value in the submit post. Although other documentation says that all form elements can be referenced as part of "app_params[name]" I was unable to get this to work. By adding a hidden input field to the rendered text (see below) the value is present.
Added by Chris Givens:
You have to wrap your controls in FBML to get the app_params to have values in the return. Example:
<fb:editor-custom label="Status">
<select name="state">
<option value="0" selected>have read</option>
<option value="1">am reading</option>
<option value="2">want to read</option>
</select>
</fb:editor-custom>
The Self-Publish Callback Handler
As part of the application settings, you define a Self-Publish callback URL. In this example I have named it SelfPublishHandler.aspx. (I will have another named OtherPublishHandler.aspx for handling the inline publisher invoked on a friend's wall.) This handler must return each of the JSON blocks described above in response to the different FB posts. The handler starts by interrogating the "method" parameter, then builds an appropriate response.
The aspx page is devoid of all content, we are simply returning a json string, not na HTML page. (Perhaps a generic handler class would suffice here?) SelfPublishHandler.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="SelfPublishHandler.aspx.cs" Inherits="SelfPublishHandler" %> <%= json %>
The code-behind generates the JSON text
SelfPublishHandler.aspx.cs
public partial class SelfPublishHandler: CanvasFBMLBasePage
{
protected string json = string.Empty;
protected void Page_PreInit(object sender, EventArgs e)
{
//this.RequireLogin = true;
}
private String getParamFromRequest(String pname)
{
String retval;
if (!String.IsNullOrEmpty(Request.Params[pname]))
{
retval = Request.Params[pname];
}
else
{
retval = "";
}
return retval;
}
protected void Page_Load(object sender, EventArgs e)
{
String fbMethod;
try
{
fbMethod = getParamFromRequest("method");
if (fbMethod == "publisher_getInterface")
{
json = processGetInterfaceRequest();
}
else if (fbMethod == "publisher_getFeedStory")
{
json = processGetFeedStoryRequest();
}
else
{
json = "unable to parse FB METHOD: " + fbMethod;
}
}
catch (Exception ex)
{
json = ex.Message + " in " + this.Page.Title;
}
}
The page load method grabs the method parameter to determine which response to generate. We are interested at this point in processGetInterfaceRequest
(SelfPublishHandler.aspx.cs continued)
private String processGetInterfaceRequest()
{
ThingGrid TG = new ThingGrid();
String fbml = TG.Render();
ThingGrid does the real work of retrieving the data and generating the interface markup. We will see the code for it shortly. The Render method renders the client-side markup based on the web user control detailed above. That text is then wrapped in the required JSON format:
the rest of the method:
String content = JSONHelper.ConvertToJSONAssociativeArray
(new Dictionary<string, string>
{ { "fbml", fbml },
{ "publishEnabled", "true" },
{ "commentEnabled", "true" } }
);
String interfaceJSON = JSONHelper.ConvertToJSONAssociativeArray
(new Dictionary<string, string>
{ { "method", "publisher_getInterface" },
{ "content", content } }
);
return interfaceJSON;
}
This text is returned to Facebook and rendered in the inline publisher UI.
Rendering the FBML
While my example uses no FBML tags, this is in fact defined to be an FBML markup, and so will support any FBML tags you require. In the following code you will see that it populates a List of Thing objects, then passes this List to the RenderFBML method, which binds that data to the web user control.
RenderFBML returns a string holding the FBML markup produced by the indicated web user control. We will look at this in more detail in the following section. Next a hidden field is appended to the markup. This field will hold the selected value as saved by the javascript in the web user control. Then, the FBML text is encoded to be compatible with the JSON format. This is not fully in complince with the JSON standard but it does suffice for most .NET markup. We remove all new lines and tabs, and unicode-escape any double-quotes.
ThingGrid.cs
public class ThingGrid
{
private List<Thing> THINGS = new List<Thing>();
public ThingGrid()
{
}
public String Render()
{
String data = "";
// get the data to be rendered, in this example it is a small set of images (<30)
// so an object data source, type List<T>, works well.
PopulateThingList();
// code not shown for brevity's sake
data = FBMLControlRenderer.RenderFBML<List<Thing>>
(string.Format("~/wucThingGrid.ascx"), THINGS );
data = string.Format(
"{0}<input type=\"hidden\" id=\"picked\" name=\"picked\" value=\"4\">",
data);
data = data.Replace("\r\n", "").Replace("\n", "").Replace("\r", "").Replace("\t", " ");
data = data.Replace(@"""", @"\u0022");
return data;
}
}
RenderFBML and the IRenderableFBML<D> Interface
The RenderFBML method allows a data object to ba passed in, and if the referenced web user control implements IRenderableFBML<D> (for any data type D) the method PopulateData (mandated by the interface contract) is invoked. (See above code for wucMyGrid.ascx.cs for implementation).
from facebook.web.FBMLControlRenderer.cs
public static string RenderFBML<D>(string path, D dataToBind)
{
Page pageHolder = new Page();
UserControl control = (UserControl)pageHolder.LoadControl(path);
if (control is IRenderableFBML<D>)
{
if (dataToBind != null)
{
((IRenderableFBML<D>)control).PopulateData(dataToBind);
}
}
pageHolder.Controls.Add(control);
StringWriter output = new StringWriter();
HttpContext.Current.Server.Execute(pageHolder, output, false);
return output.ToString();
}
Providing the Feed Story
Stories to be posted here require a feed template. Register your feed templates using the developer tool site or using code. This example assumes you have a registered template and know the template ID.
When the user of your app clicks the Submit button in the inline interface pane FB initiates a postback to the same URL, only this time the method parameter is "publisher_getFeedStory". Documentation states that page elements are returned as part of the post, in an array named app_params. As I mentioend above, I am only seeing entries for fields added "late" (see above in section 2.1 #the Web User Control) so the web user control includes javascript that populates the late-added hidden input field, and that value is referenced now, to know which Thing the user picked.
The Feed Story JSON format
{"content":
{"feed":
{"template_id":my_template_id,
"template_data":
{"template field name":"field value",
"template field name":"field value"
"images":[
{
"src" :"http://www.mysite.com/img1.jpg",
"href":"http://apps.new.facebook.com/myapp/foo.aspx"
}
{
"src" :"http://www.mysite.com/img2.jpg",
"href":"http://apps.new.facebook.com/myapp/bar.aspx"
}
]
}
}
},
"method":"publisher_getFeedStory"
}
The Self-Publish Callback Handler (again)
We saw above how the Page_Load method of the callback handler page SelfPublishHandler.aspx inspects the method parameter to determine which phase it is in. That same code now routes to method processGetFeedStoryRequest.
Getting the selected values
Because we placed javascript in our web user control, which populates a late-added hidden input field, we are able to simply pull the selected value (in this case a Thing ID) from the post parameters. It should be possible to place as many of these as necessary for more complex scenarios.
String selectedThingID = getParamFromRequest("app_params[picked]");
Generating the Feed Story JSON
In this example, we retrieve a business object that includes details needed to create the Feed Story: the Title of the Thing, a URL for the image, and a URL for the link the image will reference. Details of how this is done are not relevant to the example.
The wiki documentation for this part is pretty good, refer to it for more details: Publisher#Creating the Feed Story
Finally, here is the code that creates the Feed Template JSON response.
more code from class SelfPublishHandler.aspx.cs
private String processGetFeedStoryRequest()
{
String selectedThingID = getParamFromRequest("app_params[picked]");
// gets Thing details from data store -- details omitted
Thing myThing = (ThingDataSource.getThing(selectedThingID);
//
String imgTemp = JSONHelper.ConvertToJSONAssociativeArray(
new Dictionary<string, string>
{{"src", myThing.ImageFileName},
"href", "http://apps.facebook.com/myapp/" + myThing.ThingLink}});
String imagedata = String.Format("[{0}]", imgTemp);
String templatedata = JSONHelper.ConvertToJSONAssociativeArray(
new Dictionary<string, string>
{{"thing", myThing.Title },
{"images", imagedata} });
String feed = JSONHelper.ConvertToJSONAssociativeArray(
new Dictionary<string,string>
{{"template_id", "xxxxxxxxxxxxx"},
{"template_data", templatedata}});
String content = JSONHelper.ConvertToJSONAssociativeArray(
new Dictionary<string, string>
{{"feed", feed } });
String feedStoryJSON = JSONHelper.ConvertToJSONAssociativeArray(
new Dictionary<string, string>
{{"method", "publisher_getFeedStory" },
{"content", content } });
return feedStoryJSON;
}
