One of the great things about BlogEngine.Net is how easy it is to create widgets or plug-ins for it.
I’ve created a widget that displays the stats for a user at Run.GPS.
There are two files that go into a widget: the edit.ascx and the widget.ascx. Both files go in a folder named for the widget. That folder goes in the widgets folder off of the web project root.
The edit.ascx holds the code that is used to edit the settings for the widget. The widget.ascx holds the code for the widget itself.
Run.GPS allows users to build a badge that can be embedded in a web page or blog. The badge is configured with a series of dropdowns which change a box with HTML code at the bottom of the page. The code is surrounded in an iframe tag. The HTML can be copied and pasted into any web page to embed the badge.
For the widget to work, it should have the same settings and generate the same HTML code in an iframe tag. A quick survey of the various settings shows that the dropdowns directly change most of the values in the HTML code. For example, changing the “Units” dropdown to “Metric” changes to code to include “&units=Metric” and changing the “Map Type” dropdown to “NORMAL” changes the code to include “&mapType=NORMAL”.
The only tricky part about converting the settings to HTML code is with the width and height attributes in the HTML code. For each badge type, there are a different set of dimensions. Rather than spend too much time on this, I just hard coded the dimensions.
edit.ascx
1: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="edit.ascx.cs" Inherits="RunGPSEdit" %>
2: <table style="width: 500px">
3: <tr>
4: <td style="width: 50%">
5: User Name</td>
6: <td style="width: 50%">
7: <asp:TextBox ID="UserNameTextBox" runat="server" Width="100%"></asp:TextBox>
8: </td>
9: </tr>
10: <tr>
11: <td style="width: 50%">
12: Type</td>
13: <td style="width: 50%">
14: <asp:DropDownList ID="TypeDropDownList" runat="server" Width="100%">
15: <asp:ListItem Value="0">Calendar</asp:ListItem>
16: <asp:ListItem Value="1">Quick User Info</asp:ListItem>
17: <asp:ListItem Value="2">Total Calories Counter</asp:ListItem>
18: <asp:ListItem Value="3">Total Distance Counter</asp:ListItem>
19: <asp:ListItem Value="4">Live Tracking</asp:ListItem>
20: <asp:ListItem Value="5">Live Tracking (just the map)</asp:ListItem>
21: <asp:ListItem Value="6">Location</asp:ListItem>
22: <asp:ListItem Value="7">Training Map</asp:ListItem>
23: <asp:ListItem Value="8">Training Map & Info</asp:ListItem>
24: <asp:ListItem Value="9">Route Map</asp:ListItem>
25: </asp:DropDownList>
26: </td>
27: </tr>
28: <tr>
29: <td style="width: 50%">
30: Units</td>
31: <td style="width: 50%">
32: <asp:DropDownList ID="UnitsDropDownList" runat="server" Width="100%">
33: <asp:ListItem>Imperial</asp:ListItem>
34: <asp:ListItem>Metric</asp:ListItem>
35: </asp:DropDownList>
36: </td>
37: </tr>
38: <tr>
39: <td style="width: 50%">
40: Background</td>
41: <td style="width: 50%">
42: <asp:DropDownList ID="BackgroundDropDownList" runat="server" Width="100%">
43: <asp:ListItem Value="blue">Blue</asp:ListItem>
44: <asp:ListItem Value="green">Green</asp:ListItem>
45: <asp:ListItem Value="yellow">Yellow</asp:ListItem>
46: <asp:ListItem Value="none">None</asp:ListItem>
47: </asp:DropDownList>
48: </td>
49: </tr>
50: <tr>
51: <td style="width: 50%">
52: Map Type</td>
53: <td style="width: 50%">
54: <asp:DropDownList ID="MapTypeDropDownList" runat="server" Width="100%">
55: <asp:ListItem Value="NORMAL">Normal</asp:ListItem>
56: <asp:ListItem Value="SATELLITE">Satellite</asp:ListItem>
57: <asp:ListItem Value="HYBRID">Hybrid</asp:ListItem>
58: <asp:ListItem Value="PHYSICAL">Physical</asp:ListItem>
59: </asp:DropDownList>
60: </td>
61: </tr>
62: <tr>
63: <td style="width: 50%">
64: Map Zoom</td>
65: <td style="width: 50%">
66: <asp:DropDownList ID="MapZoomDropDownList" runat="server" Width="55px">
67: <asp:ListItem>18</asp:ListItem>
68: <asp:ListItem>17</asp:ListItem>
69: <asp:ListItem>16</asp:ListItem>
70: <asp:ListItem>15</asp:ListItem>
71: <asp:ListItem>14</asp:ListItem>
72: <asp:ListItem>13</asp:ListItem>
73: <asp:ListItem>12</asp:ListItem>
74: <asp:ListItem>11</asp:ListItem>
75: <asp:ListItem>10</asp:ListItem>
76: <asp:ListItem>9</asp:ListItem>
77: <asp:ListItem>8</asp:ListItem>
78: <asp:ListItem>7</asp:ListItem>
79: <asp:ListItem>6</asp:ListItem>
80: <asp:ListItem>5</asp:ListItem>
81: </asp:DropDownList>
82: </td>
83: </tr>
84: <tr>
85: <td style="width: 50%">
86: Reformat (experimental)</td>
87: <td style="width: 50%">
88: <asp:DropDownList ID="ReformatDropDownList" runat="server" Width="55px">
89: <asp:ListItem Value="no">No</asp:ListItem>
90: <asp:ListItem Value="yes">Yes</asp:ListItem>
91: </asp:DropDownList>
92: </td>
93: </tr>
94: <tr>
95: <td style="width: 50%">
96: Reformat Color</td>
97: <td style="width: 50%">
98: <asp:TextBox ID="ReformatColorTextBox" runat="server" Width="100%"></asp:TextBox>
99: </td>
100: </tr>
101: </table>
For the edit.ascx, I simply copied the user interface from the badge configuration web page. I made the values of the various drop down items exactly what they needed to be in the HTML code. That way I could copy the values directly into the generation HTML. The only exception is for the “Type” field. The type field controls which type of badge is going to be generated. This setting also controls the dimensions of the badge. I’ve created an array of dimensions so the item values for the “Type” field will be indexes into this array.
edit.ascx.cs
1: using System;
2: using System.Collections.Generic;
3: using System.Web;
4: using System.Web.UI;
5: using System.Web.UI.WebControls;
6: using BlogEngine.Core;
7: using System.Collections.Specialized;
8:
9: public partial class RunGPSEdit : WidgetEditBase {
10: protected void Page_Load(object sender, EventArgs e) {
11: if (!Page.IsPostBack) {
12: StringDictionary settings = GetSettings();
13:
14: if (settings.ContainsKey("UserName")) {
15: UserNameTextBox.Text = settings["UserName"];
16: }
17:
18: if (settings.ContainsKey("WidgetType")) {
19: TypeDropDownList.SelectedValue = settings["WidgetType"];
20: }
21:
22: if (settings.ContainsKey("Units")) {
23: UnitsDropDownList.SelectedValue = settings["Units"];
24: }
25:
26: if (settings.ContainsKey("Color")) {
27: BackgroundDropDownList.SelectedValue = settings["Color"];
28: }
29:
30: if (settings.ContainsKey("MapType")) {
31: MapTypeDropDownList.SelectedValue = settings["MapType"];
32: }
33:
34: if (settings.ContainsKey("MapZoom")) {
35: MapZoomDropDownList.SelectedValue = settings["MapZoom"];
36: }
37:
38: if (settings.ContainsKey("Reformat")) {
39: ReformatDropDownList.SelectedValue = settings["Reformat"];
40: }
41:
42: if (settings.ContainsKey("ReformatColor")) {
43: ReformatColorTextBox.Text = settings["ReformatColor"];
44: }
45:
46: }
47: }
48:
49: public override void Save() {
50: StringDictionary settings = GetSettings();
51:
52: settings["UserName"] = UserNameTextBox.Text;
53: settings["WidgetType"] = TypeDropDownList.SelectedValue;
54: settings["Units"] = UnitsDropDownList.SelectedValue;
55: settings["Color"] = BackgroundDropDownList.SelectedValue;
56: settings["MapType"] = MapTypeDropDownList.SelectedValue;
57: settings["MapZoom"] = MapZoomDropDownList.SelectedValue;
58: settings["Reformat"] = ReformatDropDownList.SelectedValue;
59: settings["ReformatColor"] = ReformatColorTextBox.Text;
60:
61: SaveSettings(settings);
62: }
63: }
The code behind for edit.ascx is pretty straight forward.
The edit control itself must derive from WidgetEditBase. WidgetEditBase has an abstract method Save() that must be overwritten. In the Save() method, the settings are moved from the individual user interface controls into the settings for the Widget. The settings for the Widget are stored in a StringDictionary and can be retreived by calling GetSettings() as done on line 50. The last thing to do is save the settings by calling SaveSettings() as done on line 61.
widget.ascx
1: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="widget.ascx.cs" Inherits="RunGPSWidget" %>
2: <asp:PlaceHolder ID="WidgetPlaceHolder" runat="server"></asp:PlaceHolder>
The widget itself is as simple as it gets. It has one PlaceHolder control on the form and nothing else. I’ll insert the HTML that gets generated into this PlaceHolder when the widget is run.
widget.ascx.cs
The code behind for the widget is the most complicated part, but only in comparison to the rest of it.
1: public partial class RunGPSWidget : WidgetBase {
Like the edit control, the widget itself must derive from an abstract base. In this case it is the WidgetBase.
12: private class WidgetType {
13: public string scriptName = "";
14: public string scriptWidth = "";
15: public string scriptHeight = "";
16: public string frameWidth = "";
17: public string frameHeight = "";
18:
19: public WidgetType(string scriptName, string scriptWidth, string scriptHeight, string frameWidth, string frameHeight) {
20: this.scriptName = scriptName;
21: this.scriptWidth = scriptWidth;
22: this.scriptHeight = scriptHeight;
23: this.frameWidth = frameWidth;
24: this.frameHeight = frameHeight;
25: }
26: }
27:
28: private static WidgetType[] widgetTypes = {new WidgetType("embedCalendar.jsp", "490", "690", "500", "700"),
29: new WidgetType("embedQuickUserInfo.jsp", "270", "270", "280", "280"),
30: new WidgetType("embedCaloriesCounter.jsp", "140", "35", "150", "45"),
31: new WidgetType("embedDistanceCounter.jsp", "140", "35", "150", "45"),
32: new WidgetType("embedWhereAmI.jsp", "270", "370", "280", "380"),
33: new WidgetType("embedWhereAMIMapOnly.jsp", "300", "300", "310", "310"),
34: new WidgetType("embedLocation.jsp", "490", "10", "500", "20"),
35: new WidgetType("embedTrainingMap.jsp", "320", "320", "330", "330"),
36: new WidgetType("embedTrainingMapAndInfo.jsp", "490", "690", "500", "700"),
37: new WidgetType("embedRouteMap.jsp", "410", "410", "420", "420")
38: };
Lines 12 through 38 are where the dimensions for the various badge types are hard coded. First I declare a small class to hold the values for each badge type called WidgetType. The WidgetType class holds five strings. One is for the name of the script that will be called on the Run.GPS server and the other four are the dimensions for the badge.
There are three abstract members on WidgetBase that must be overridden. The first two are the properties Name and IsEditable.
40: public override string Name {
41: get { return "RunGPS"; }
42: }
43:
44: public override bool IsEditable {
45: get { return true; }
46: }
The Name property must return the same as the name of the folder that the widget files are in.
The IsEditable property should return true if the widget has any settings. Basically, it should return true if there is an edit.ascx for the widget. There is one, so I returned true on line 45.
The heavy lifting for the widget happens in the override for the method LoadWidget().
48: public override void LoadWidget() {
49: StringDictionary settings = GetSettings();
50:
51: int ndx = 0;
52: WidgetType widgetType = widgetTypes[ndx];
53:
54: if (settings.ContainsKey("WidgetType")) {
55: int.TryParse(settings["WidgetType"], out ndx);
56: if (ndx >= 0 && ndx < widgetTypes.Length) {
57: widgetType = widgetTypes[ndx];
58: }
59: }
60:
61: string userName = "";
62: string units = "";
63: string color = "";
64: string mapType = "";
65: string mapZoom = "";
66: string leftOffset = "0";
67: string reformat = "no";
68: string reformatColor = "";
69:
70: if (settings.ContainsKey("UserName")) {
71: userName = settings["UserName"];
72: }
73:
74: if (settings.ContainsKey("Units")) {
75: units = settings["Units"];
76: }
77:
78: if (settings.ContainsKey("Color")) {
79: color = settings["Color"];
80: }
81:
82: if (settings.ContainsKey("MapType")) {
83: mapType = settings["MapType"];
84: }
85:
86: if (settings.ContainsKey("MapZoom")) {
87: mapZoom = settings["MapZoom"];
88: }
89:
90: if (settings.ContainsKey("Reformat")) {
91: reformat = settings["Reformat"];
92: }
93:
94: if (settings.ContainsKey("ReformatColor")) {
95: reformatColor = settings["ReformatColor"];
96: }
97:
98: int width = 0;
99: int.TryParse(widgetType.frameWidth, out width);
100:
101: if (width >= 250) {
102: leftOffset = "-12";
103: }
104:
105: string src = string.Format("http://www.gps-sport.net/{0}?userName={1}&width={2}&height={3}&color={4}&units={5}&mapType={6}&routeID=&zoom={7}",
106: widgetType.scriptName,
107: userName,
108: widgetType.scriptWidth,
109: widgetType.scriptHeight,
110: color,
111: units,
112: mapType,
113: mapZoom);
114:
115:
116: string content = string.Format("<iframe style=\"position: relative; left: {3}px\"src=\"{0}\" width=\"{1}\" height=\"{2}\" scrolling=\"no\" align=\"center\" valign=\"top\" frameborder=\"0\"></iframe>",
117: src,
118: widgetType.frameWidth,
119: widgetType.frameHeight,
120: leftOffset);
121:
122: LiteralControl html;
123:
124: if (userName != "" && string.Compare(reformat, "yes", true) == 0) {
125: string saveContent = content;
126:
127: try {
128: WebClient webClient = new WebClient();
129: byte[] pageBuffer = webClient.DownloadData(src);
130: UTF8Encoding utf8 = new UTF8Encoding();
131: string pageHTML = utf8.GetString(pageBuffer);
132:
133: int tableStart = pageHTML.IndexOf("<table", StringComparison.OrdinalIgnoreCase);
134: if (tableStart >= 0) {
135: int tableEnd = pageHTML.LastIndexOf("</table>") + 8;
136:
137: pageHTML = pageHTML.Substring(tableStart, tableEnd - tableStart);
138:
139: string url = string.Format("http://www.gps-sport.net/users/{0}?orig=embedWhereAmI", userName);
140:
141: content = string.Format("<table style=\"width: 100%;\" bgcolor=\"{0}\"><tr><td><table width=\"100%\"><tr><td align=\"center\"><a target=\"_blank\" href=\"{1}\"><img src=\"http://www.gps-sport.net/images/logos/logo.png\"></a></td></tr></table>{2}</td></tr></table>", reformatColor, url, pageHTML);
142: }
143: }
144: catch {
145: content = saveContent;
146: }
147:
148: }
149:
150: html = new LiteralControl(content);
151:
152: WidgetPlaceHolder.Controls.Add(html);
153: }
Same as in the edit control, the first thing I do, in line 49, is get the StringDictionary containing the settings for the widget. In lines 54 through 59, I get the setting for WidgetType, which is an index into the array of WidgetType, and grab the correct hard-coded WidgetType from the array.
The next 40 or so lines are all about pulling in the settings for the widget. Theoretically, if any of the settings is contained in the StringDictionary, they all should be there, but I verify each one individually to avoid any errors.
Line 105 builds the URL for the script that will be called from Run.GPS.
Line 116 wraps that URL in the iframe just like the badge generator on the Run.GPS site. Basically, the widget is going to return the exact same code as the generator on the website.
Line 122 declares a LiteralControl that will hold the final output for the widget.
And finally, on line 152, I add the LiteralControl containing the completed HTML code into the place holder on the control.
Hacking
Now we get to the hacking part. You see, Run.GPS actually has two websites. One is the storefront for their GPS software for Windows Mobile devices. The other is the community server for users of this software. The URL for the community server is actually GPS-Sport.net, but the entire site is branded Run.GPS. It is my account on this community server site that I want to link to. The link at the bottom of the badge goes to the community server, but the large graphic on the top, goes to the storefront. I want to get the results of the script at Run.GPS and reformat it to my liking.
124: if (userName != "" && string.Compare(reformat, "yes", true) == 0) {
125: string saveContent = content;
126:
127: try {
128: WebClient webClient = new WebClient();
129: byte[] pageBuffer = webClient.DownloadData(src);
130: UTF8Encoding utf8 = new UTF8Encoding();
131: string pageHTML = utf8.GetString(pageBuffer);
132:
133: int tableStart = pageHTML.IndexOf("<table", StringComparison.OrdinalIgnoreCase);
134: if (tableStart >= 0) {
135: int tableEnd = pageHTML.LastIndexOf("</table>") + 8;
136:
137: pageHTML = pageHTML.Substring(tableStart, tableEnd - tableStart);
138:
139: string url = string.Format("http://www.gps-sport.net/users/{0}?orig=embedWhereAmI", userName);
140:
141: content = string.Format("<table style=\"width: 100%;\" bgcolor=\"{0}\"><tr><td><table width=\"100%\"><tr><td align=\"center\"><a target=\"_blank\" href=\"{1}\"><img src=\"http://www.gps-sport.net/images/logos/logo.png\"></a></td></tr></table>{2}</td></tr></table>", reformatColor, url, pageHTML);
142: }
143: }
144: catch {
145: content = saveContent;
146: }
147:
148: }
At line 124, I check to make sure that we have a username and check to see if the hack has been turned on. I added an extra setting on the edit control that allows the hack to be turned on or off.
If the hack is running, the first thing I do is make a copy of the current output. Then I try to hack it. At line 128, I create a WebClient and then use it to download the results into a buffer. Normally the HTML that is sent to the web browser is a call to script. The script is actually called from the web browser and the results are sent directly to the web browser. What I’ve done here is to get the results of that script and put them into memory while still generating the content of the widget on the server. Now I can do whatever I want with it.
I’ve examined some of the responses and determined that the actual data in the badge is contained in a table and the cosmetics are built around that table. This makes it pretty easy to grab just the data and reformat it. At line 133 through 135, I check to see if I can find a table in the HTML. If I do, I pull out the table and disregard the rest. I then, at line 141, wrap my own table around the one I got from the script and put in the logo with a link directly to my profile at the community server. Of course, there is a lot more I can do here if I felt like it. The statistics can easily be parsed out of the table and reformatted in infinite ways. A more complicated widget would do exactly that.
Conclusion
Putting together a widget for BlogEngine.net is fairly easy. This makes the BlogEngine.net platform very attractive to .Net programmers who can easily leverage the power of the .Net framework in extending the BlogEngine framework. Creating the widget is just a matter of subclassing one or two base classes and overriding a few members. With a WebClient control, it’s easy to download content from the internet and then use the full power of the .Net framework to present it in a concise and attractive format.
Download: RunGPS.zip (4KB)