Getting Twitter Profile Image Via C# With 1.1 API

May 1 2013

If you know someone's Twitter handle and would like to display their twitter avatar on your website, here's some code to get the URL of their avatar.  First, you'll need to register at http://dev.twitter.com and acquire a ConsumerKey, ConsumerSecret, Token and TokenSecret -- now that Twitter supports application only authentication, there isn't any handshaking involved; you just need to craft up the right OAuthCredentials for a ProtectedResource, which the Hammock library does for you. Love that library: you can get it here: https://github.com/danielcrenna/hammock or grab it as a NuGet package: http://nuget.org/packages/Hammock

Here’s the code; nothing too fancy:

using System;
using Hammock;
using Hammock.Authentication.OAuth;
using Hammock.Web;
using Newtonsoft.Json.Linq;

namespace ExternalServices
{
    public class TwitterAvatarLookup : ITwitterAvatarLookup
    {
        const string ConsumerKey = "";
        const string ConsumerSecret = "";
        const string Token = "";
        const string TokenSecret = "";

        public string GetTwitterAvatarUrl(string twitterHandle)
        {
            string avatarUrl = string.Empty;
            var request = new RestRequest
            {
                Credentials = new OAuthCredentials
                {
                    Type = OAuthType.ProtectedResource,
                    SignatureMethod = OAuthSignatureMethod.HmacSha1,
                    ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
                    ConsumerKey = ConsumerKey,
                    ConsumerSecret = ConsumerSecret,
                    Token = Token,
                    TokenSecret = TokenSecret,
                }
            };
            request.Path =
                string.Format(
                    "https://api.twitter.com/1.1/users/lookup.json?screen_name={0}&include_entities=0&include_rts=0",
                    twitterHandle);

            request.Method = WebMethod.Get;
            RestClient client = new RestClient();
            try
            {
                RestResponse response = client.Request(request);
                JArray jArray = JArray.Parse(response.Content);
                avatarUrl = (string)jArray[0]["profile_image_url_https"];

            }
            catch (Exception)
            {
                return "default.png";
            }
            return avatarUrl;
        }

    }
}

If you know more than one handle whose avatar you need to get, the API supports passing multiple user handles; see https://dev.twitter.com/docs/api/1.1/get/users/lookup

Saving a PDFSharp PDF File To Azure Blob Storage

March 1 2013

Love the PDFSharp library. Here’s how I went about saving a PDF generated with that library to Azure blob storage:

const bool unicode = false;
const PdfFontEmbedding embedding = PdfFontEmbedding.Always;
PdfDocumentRenderer pdfRenderer = new PdfDocumentRenderer(unicode, embedding);
pdfRenderer.Document = document;
pdfRenderer.RenderDocument();
MemoryStream memStream = new MemoryStream();
pdfRenderer.PdfDocument.Save(memStream,false);

var client = new CloudBlobClient(new Uri("http://*.blob.core.windows.net", UriKind.Absolute),
new StorageCredentialsAccountAndKey("*",
"..."));

var container = client.GetContainerReference("temp");
memStream.Seek(0, SeekOrigin.Begin);
string filename = DateTime.Now.ToString().GetHashCode().ToString("x") + ".pdf";
var pdf = container.GetBlobReference(filename);
pdf.Properties.ContentType = "application/pdf";
pdf.UploadFromStream(memStream, new BlobRequestOptions { Timeout = TimeSpan.FromMinutes(10) });
memStream.Close();


The crux of the code is the line where pass false to the Save method of PdfDocument, which keeps the memory stream open. And, then, before giving that stream to the Azure SDK method, you need to rewind the stream to the beginning. Other than that, all pretty boilerplate.

Capture ClickOnce File Downloads With Event Tracking In Google Analytics

March 1 2013

This flummoxed me for a bit, so I figured I post it. If you wire the event up to an onClick handler as the docs suggest:

<a href="app.application" onClick="_gaq.push(['_trackEvent', 'ClickOnce', 
'Download']);">Play</a>

Your event will never fire. The trick is to add the target attribute and set it to _blank which opens a new tab in the browser and immediately closes it:

<a href="app.application" target="_blank" onClick="_gaq.push(['_trackEvent', 'ClickOnce',
'Download']);">Play</a>
 

Windows Azure Table Storage Emulator UpdateObject Error

January 21 2013

Was getting this:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <code>InvalidInput</code>
  <message xml:lang="en-US">One of the request inputs is not valid.</message>
</error>

When calling UpdateObject from the Azure SDK when using the table storage emulator. Turns out the emulator doesn’t exactly emulate as per MSDN documentation: http://msdn.microsoft.com/en-us/library/gg433135.aspx “The storage emulator does not support Insert-Or-Replace Entity or Insert-Or-Merge Entity, known as upsert features.”

So, to work around, I changed the code to delete then reinsert.  Or not. Turned out I had lots of concurrancy problems with doing a delete/insert which were resolved with the more transactional upsert. 

Sorting ListBlobs By LastModifiedUtc

December 3 2012

I had a list of JSON objects stored as individual blobs in Azure blob storage. I needed to get them out of blob storage sorted by last modified time and then reconstitute them as an array of JSON objects. Hit a few curiosities in writing this code, which someone else may benefit from. Without further ado, here’s the code:

            CloudBlobDirectory dir = eventContainer.GetDirectoryReference("http://---");
            SortedDictionary<DateTime, string> dictionary = new SortedDictionary<DateTime, string>();
            foreach (CloudBlob blob in dir.ListBlobs())
            {
                if (blob.Name == string.Format("{0}/$$$.$$$", "live")) continue;
                dictionary.Add(blob.Properties.LastModifiedUtc, blob.DownloadText());
            }
            StringBuilder json = new StringBuilder();
            json.Append("Callback([");
            foreach (string s in dictionary.Values)
            {
                json.Append(s);
                json.Append(",");
            }
            json.Remove(json.Length - 1, 1);
            json.Append("])");
            CloudBlob liveJson = dir.GetBlobReference("live.json");
            liveJson.Properties.CacheControl = cacheControl;
            liveJson.UploadText(json.ToString());

Okay, so what is going on here?

I start out with a nifty SortedDictionary, which will sort my items by the key of the dictionary as I add them.

I then call ListBlobs(). There’s this peculiar oddity with Blob Storage when you use CloudXplorer where this ghost file name $$$.$$$ gets created, which is why I have to check the name of the file. I then throw the string and the date into the dictionary.

Once that finishes, I iterate the dictionary, adding the callback and syntax for making a json array. Get rid of the final comma and then throw the whole deal back into blob storage. Hoorah!

At first, I was actually deserializing the strings into objects using JSON.Net and then I realized there was no reason to do that when all I needed to do was manipulate strings.

Converting New Twitter Search Result To Old Format in C# Using JSON.NET

November 5 2012

Been playing with the new Twitter API, version 1.1, and want to preserve a bunch of code, so I wrote a routine to convert the new format to the old format. Maybe it’ll save someone time. (Note that I didn’t convert the metadata because I didn’t need it.)

Here’s the code which uses JSON.NET:

            JObject newapiresults = null;
            newapiresults = JObject.Parse(response.Content);
            JArray tweets = new JArray();
            foreach (JObject status in tweets)
            {
                JObject oldStatus = new JObject();
                oldStatus["created_at"] = status["created_at"];
                oldStatus["from_user"] = status["user"]["screen_name"];
                oldStatus["from_user_id"] = status["user"]["id"];
                oldStatus["from_user_id_str"] = status["user"]["id_str"];
                oldStatus["from_user_id_name"] = status["user"]["name"];
                oldStatus["geo"] = status["geo"];
                oldStatus["id"] = status["id"];
                oldStatus["id_str"] = status["id_str"];
                oldStatus["iso_language_code"] = status["metadata"]["iso_language_code"];
                oldStatus["metadata"] = status["metadata"];
                oldStatus["profile_image_url"] = status["user"]["profile_image_url"];
                oldStatus["profile_image_url_https"] = status["user"]["profile_image_url_https"];
                oldStatus["source"] = status["source"];
                oldStatus["text"] = status["text"];
                oldStatus["to_user"] = status["in_reply_to_screen_name"];
                oldStatus["to_user_id"] = status["in_reply_to_user_id"];
                oldStatus["to_user_id_str"] = status["in_reply_to_user_id_str"];
                oldStatus["to_user_name"] = status["in_reply_to_screen_name"];
                oldStatus["in_reply_to_status_id"] = status["in_reply_to_status_id"];
                oldStatus["in_reply_to_status_id_str"] = status["in_reply_to_status_id_str"];
                results.Add(oldStatus);


            }
            JObject result = new JObject();
            result["results"] = results;
            //do something with the string
            UploadText(result.ToString());

For what it is worth, here’s the old JSON and then the new JSON – wow the new JSON is a lot more verbose!

{ "completed_in" : 0.029000000000000001,
  "max_id" : 265608574640738304,
  "max_id_str" : "265608574640738304",
  "next_page" : "?page=2&max_id=265608574640738304&q=%40twitterapi%20-via",
  "page" : 1,
  "query" : "%40twitterapi+-via",
  "refresh_url" : "?since_id=265608574640738304&q=%40twitterapi%20-via",
  "results" : [ { "created_at" : "Tue, 06 Nov 2012 00:16:33 +0000",
        "from_user" : "morgules",
        "from_user_id" : 636921780,
        "from_user_id_str" : "636921780",
        "from_user_name" : "Дмитрий Моргулес",
        "geo" : null,
        "id" : 265608574640738304,
        "id_str" : "265608574640738304",
        "iso_language_code" : "ru",
        "metadata" : { "result_type" : "recent" },
        "profile_image_url" : "http://a0.twimg.com/profile_images/2647733681/0daea79d4d5858a39bb1801d64b20e14_normal.jpeg",
        "profile_image_url_https" : "https://si0.twimg.com/profile_images/2647733681/0daea79d4d5858a39bb1801d64b20e14_normal.jpeg",
        "source" : "<a href="http://twitter.com/tweetbutton">Tweet Button</a>",
        "text" : "Руслан Нурисламов: безрукий барабанщик из Златоуста | Общество | Слово http://t.co/ziOSiA78 с помощью @twitterapi",
        "to_user" : null,
        "to_user_id" : 0,
        "to_user_id_str" : "0",
        "to_user_name" : null
      },
      { "created_at" : "Tue, 06 Nov 2012 00:13:12 +0000",
        "from_user" : "oxkarlomejor",
        "from_user_id" : 221827509,
        "from_user_id_str" : "221827509",
        "from_user_name" : "oscar  david ",
        "geo" : null,
        "id" : 265607731417870336,
        "id_str" : "265607731417870336",
        "iso_language_code" : "es",
        "metadata" : { "result_type" : "recent" },
        "profile_image_url" : "http://a0.twimg.com/profile_images/1602975094/Imagen_010_normal.jpg",
        "profile_image_url_https" : "https://si0.twimg.com/profile_images/1602975094/Imagen_010_normal.jpg",
        "source" : "<a href="http://twitter.com/">web</a>",
        "text" : "@twitterapi quiero ser Famoso",
        "to_user" : "twitterapi",
        "to_user_id" : 6253282,
        "to_user_id_str" : "6253282",
        "to_user_name" : "Twitter API"
      }]}
 
And here’s the new JSON:
{ "search_metadata" : { "completed_in" : 0.089999999999999997,
      "count" : 100,
      "max_id" : 265255057749053440,
      "max_id_str" : "265255057749053440",
      "next_results" : "?max_id=265212014639603711&q=nekocase&count=100&include_entities=1",
      "query" : "nekocase",
      "refresh_url" : "?since_id=265255057749053440&q=nekocase&include_entities=1",
      "since_id" : 0,
      "since_id_str" : "0"
    },
  "statuses" : [ { "contributors" : null,
        "coordinates" : null,
        "created_at" : "Mon Nov 05 00:51:49 +0000 2012",
        "entities" : { "hashtags" : [  ],
            "media" : [ { "display_url" : "pic.twitter.com/PEzikMU5",
                  "expanded_url" : "http://twitter.com/jtspicer/status/265255057749053440/photo/1",
                  "id" : 265255057753247745,
                  "id_str" : "265255057753247745",
                  "indices" : [ 115,
                      135
                    ],
                  "media_url" : "http://p.twimg.com/A65gEdRCEAEf0xo.jpg",
                  "media_url_https" : "https://p.twimg.com/A65gEdRCEAEf0xo.jpg",
                  "sizes" : { "large" : { "h" : 766,
                          "resize" : "fit",
                          "w" : 1024
                        },
                      "medium" : { "h" : 449,
                          "resize" : "fit",
                          "w" : 600
                        },
                      "small" : { "h" : 254,
                          "resize" : "fit",
                          "w" : 340
                        },
                      "thumb" : { "h" : 150,
                          "resize" : "crop",
                          "w" : 150
                        }
                    },
                  "type" : "photo",
                  "url" : "http://t.co/PEzikMU5"
                } ],
            "urls" : [  ],
            "user_mentions" : [ { "id" : 126406217,
                  "id_str" : "126406217",
                  "indices" : [ 20,
                      29
                    ],
                  "name" : "Neko Case",
                  "screen_name" : "NekoCase"
                } ]
          },
        "favorited" : false,
        "geo" : null,
        "id" : 265255057749053440,
        "id_str" : "265255057749053440",
        "in_reply_to_screen_name" : null,
        "in_reply_to_status_id" : null,
        "in_reply_to_status_id_str" : null,
        "in_reply_to_user_id" : null,
        "in_reply_to_user_id_str" : null,
        "metadata" : { "iso_language_code" : "en",
            "result_type" : "recent"
          },
        "place" : null,
        "possibly_sensitive" : false,
        "retweet_count" : 0,
        "retweeted" : false,
        "source" : "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
        "text" : "The best way to get @NekoCase to RT you is to tweet a context-free photo of an adorable animal. Here goes nothing! http://t.co/PEzikMU5",
        "truncated" : false,
        "user" : { "contributors_enabled" : false,
            "created_at" : "Mon Feb 23 19:57:21 +0000 2009",
            "default_profile" : false,
            "default_profile_image" : false,
            "description" : "Writer of things. Lover of music. Appreciator of film. Unabashed know-it-all. Street walkin' cheetah with a heart full of napalm. And so on. Views=mine.",
            "entities" : { "description" : { "urls" : [  ] } },
            "favourites_count" : 768,
            "follow_request_sent" : null,
            "followers_count" : 360,
            "following" : null,
            "friends_count" : 336,
            "geo_enabled" : false,
            "id" : 21686478,
            "id_str" : "21686478",
            "is_translator" : false,
            "lang" : "en",
            "listed_count" : 8,
            "location" : "Columbus, Ohio (Short North)",
            "name" : "Justin Spicer",
            "notifications" : null,
            "profile_background_color" : "D3D9DB",
            "profile_background_image_url" : "http://a0.twimg.com/profile_background_images/243592391/x3ce2eb796060291bf2a9d40db53a85f.jpg",
            "profile_background_image_url_https" : "https://si0.twimg.com/profile_background_images/243592391/x3ce2eb796060291bf2a9d40db53a85f.jpg",
            "profile_background_tile" : false,
            "profile_banner_url" : "https://si0.twimg.com/profile_banners/21686478/1351622268",
            "profile_image_url" : "http://a0.twimg.com/profile_images/2785722127/42dbe3599093209c9abab988e56f2cdf_normal.jpeg",
            "profile_image_url_https" : "https://si0.twimg.com/profile_images/2785722127/42dbe3599093209c9abab988e56f2cdf_normal.jpeg",
            "profile_link_color" : "DB6995",
            "profile_sidebar_border_color" : "5AC3E9",
            "profile_sidebar_fill_color" : "2D1E29",
            "profile_text_color" : "A177AB",
            "profile_use_background_image" : true,
            "protected" : false,
            "screen_name" : "jtspicer",
            "show_all_inline_media" : true,
            "statuses_count" : 11964,
            "time_zone" : "Eastern Time (US & Canada)",
            "url" : null,
            "utc_offset" : -18000,
            "verified" : false
          }
      },
      { "contributors" : null,
        "coordinates" : null,
        "created_at" : "Mon Nov 05 00:42:28 +0000 2012",
        "entities" : { "hashtags" : [  ],
            "urls" : [  ],
            "user_mentions" : [ { "id" : 126406217,
                  "id_str" : "126406217",
                  "indices" : [ 0,
                      9
                    ],
                  "name" : "Neko Case",
                  "screen_name" : "NekoCase"
                },
                { "id" : 134073162,
                  "id_str" : "134073162",
                  "indices" : [ 10,
                      21
                    ],
                  "name" : "brigid ",
                  "screen_name" : "MBbyBrigid"
                }
              ]
          },
        "favorited" : false,
        "geo" : null,
        "id" : 265252709555376128,
        "id_str" : "265252709555376128",
        "in_reply_to_screen_name" : "NekoCase",
        "in_reply_to_status_id" : 265237602242818049,
        "in_reply_to_status_id_str" : "265237602242818049",
        "in_reply_to_user_id" : 126406217,
        "in_reply_to_user_id_str" : "126406217",
        "metadata" : { "iso_language_code" : "en",
            "result_type" : "recent"
          },
        "place" : null,
        "retweet_count" : 0,
        "retweeted" : false,
        "source" : "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
        "text" : "@NekoCase @mbbybrigid thank you so much for sharing the fundraiser!",
        "truncated" : false,
        "user" : { "contributors_enabled" : false,
            "created_at" : "Thu Dec 16 20:06:39 +0000 2010",
            "default_profile" : false,
            "default_profile_image" : false,
            "description" : "The owner/crafter/dreamer behind Cambridge's first stitch lounge and craft studio. Avid gardener. Opinionated stitcher. And I want you to make something...",
            "entities" : { "description" : { "urls" : [  ] },
                "url" : { "urls" : [ { "expanded_url" : null,
                          "indices" : [ 0,
                              27
                            ],
                          "url" : "http://gatherhereonline.com"
                        } ] }
              },
            "favourites_count" : 520,
            "follow_request_sent" : null,
            "followers_count" : 1024,
            "following" : null,
            "friends_count" : 437,
            "geo_enabled" : false,
            "id" : 227419407,
            "id_str" : "227419407",
            "is_translator" : false,
            "lang" : "en",
            "listed_count" : 47,
            "location" : "Cambridge, MA",
            "name" : "virginia b. johnson",
            "notifications" : null,
            "profile_background_color" : "C0DEED",
            "profile_background_image_url" : "http://a0.twimg.com/profile_background_images/293836111/spools_of_thread.jpg",
            "profile_background_image_url_https" : "https://si0.twimg.com/profile_background_images/293836111/spools_of_thread.jpg",
            "profile_background_tile" : true,
            "profile_image_url" : "http://a0.twimg.com/profile_images/1192172838/etsy_gather_here_avatar_normal.jpg",
            "profile_image_url_https" : "https://si0.twimg.com/profile_images/1192172838/etsy_gather_here_avatar_normal.jpg",
            "profile_link_color" : "0084B4",
            "profile_sidebar_border_color" : "C0DEED",
            "profile_sidebar_fill_color" : "DDEEF6",
            "profile_text_color" : "333333",
            "profile_use_background_image" : true,
            "protected" : false,
            "screen_name" : "gather_here",
            "show_all_inline_media" : false,
            "statuses_count" : 7655,
            "time_zone" : "Central Time (US & Canada)",
            "url" : "http://gatherhereonline.com",
            "utc_offset" : -21600,
            "verified" : false
          }
      }]}

Welcome to BlogEngine.NET 2.8

October 15 2012

If you see this post it means that BlogEngine.NET 2.8 is running and the hard part of creating your own blog is done. There is only a few things left to do.

Write Permissions

To be able to log in to the blog and writing posts, you need to enable write permissions on the App_Data folder. If your blog is hosted at a hosting provider, you can either log into your account’s admin page or call the support. You need write permissions on the App_Data folder because all posts, comments, and blog attachments are saved as XML files and placed in the App_Data folder. 

If you wish to use a database to to store your blog data, we still encourage you to enable this write access for an images you may wish to store for your blog posts.  If you are interested in using Microsoft SQL Server, MySQL, SQL CE, or other databases, please see the BlogEngine wiki to get started.

Security

When you've got write permissions to the App_Data folder, you need to change the username and password. Find the sign-in link located either at the bottom or top of the page depending on your current theme and click it. Now enter "admin" in both the username and password fields and click the button. You will now see an admin menu appear. It has a link to the "Users" admin page. From there you can change the username and password.  Passwords are hashed by default so if you lose your password, please see the BlogEngine wiki for information on recovery.

Configuration and Profile

Now that you have your blog secured, take a look through the settings and give your new blog a title.  BlogEngine.NET 2.8 is set up to take full advantage of of many semantic formats and technologies such as FOAF, SIOC and APML. It means that the content stored in your BlogEngine.NET installation will be fully portable and auto-discoverable.  Be sure to fill in your author profile to take better advantage of this.

Themes, Widgets & Extensions

One last thing to consider is customizing the look of your blog.  We have a few themes available right out of the box including two fully setup to use our new widget framework.  The widget framework allows drop and drag placement on your side bar as well as editing and configuration right in the widget while you are logged in.  Extensions allow you to extend and customize the behaivor of your blog.  Be sure to check the BlogEngine.NET Gallery at dnbegallery.org as the go-to location for downloading widgets, themes and extensions.

On the web

You can find BlogEngine.NET on the official website. Here you'll find tutorials, documentation, tips and tricks and much more. The ongoing development of BlogEngine.NET can be followed at CodePlex where the daily builds will be published for anyone to download.  Again, new themes, widgets and extensions can be downloaded at the BlogEngine.NET gallery.

Good luck and happy writing.

The BlogEngine.NET team

Getting Twitter Avatar Via C#

October 5 2012
 
 
Until Twitter deprecates their version 1 api, getting an avatar from Twitter anonymously serverside with C# is a snap. A little goofy since it returns a 302 so you have to set AllowAutoRedirect to false and then look in the Header location to get the URI:
private string GetTwitterAvatarUrl(string twitterHandle)
        {
            string avatarUrl = string.Empty;
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format("https://api.twitter.com/1/users/profile_image?screen_name={0}", twitterHandle));
            request.AllowAutoRedirect = false;
            request.Method = "GET";
            try
            {
                WebResponse response = request.GetResponse();
                if (response.Headers["Location"] != null)
                    avatarUrl = response.Headers["Location"];
            }
            catch (WebException) {}
            
            return avatarUrl;
        }

WordPress Audio Player Now Falls Back To HTML5

August 15 2012

I realized people on iOS and other devices that don’t support Flash couldn’t use the embedded Flash player for listening to audio when using the WordPress Audio Player plugin.  I wanted to customize it so that sites would fall back to HTML5 if Flash wasn’t present. I found a post in the WordPress forums of someone who did this here.

However, his script didn’t work as it was for two reasons. First, he was Base64 encoding the url to the mp3 — not sure why. So I remmed out that code. Second, he was replacing the parent element with the <audio> tag, which was blowing away the entire post!  By replacing the element witht the id of the audio tag itself, walla!  Everything works!

You can download the modified file here and minify if you’d like.

Skinless jPlayer Implementation

August 15 2012

I recently implemented a skinless audio player using jPlayer that plays m4a files that it retrieves from the iTunes Search API.

I am using jPlayer 2.1.0 and the main gotchas I hit were problems with not having the latest version of Flash on target computers.  jPlayer requires Flash 10 or higher for fallback, which it seems to do for m4a files in Firefox.

Anyway, here’s the code if you are interested. Here’s where I instantiate the jPlayer. The only parameters I pass are the supplied parameter. I got away with the defaults for everything else.

$("#jplayer").jPlayer({
ready: function() {
$("#jp_container .track-default").click();
},
supplied: "m4a, m4v"
});
Then, here’s how I wire up the click handler, toggling between the play and pause icons:
var current;
 $(".play").click(function(e) {
 $("#jplayer").jPlayer("setMedia", {
 m4a: $(this).attr("href")
 });

 if (current == this) {
 $(current).empty();
 $(current).append("<img style='vertical-align:middle;' 
border='0' src='/images/ic_menu_play.png'/>"
); $("#jplayer").jPlayer("pause"); current = null; return false; } $("#jplayer").jPlayer("play"); if (current != null) { $(current).empty(); $(current).append("<img style='vertical-align:middle;'
border='0' src='/images/ic_menu_play.png'/>"
); } $(this).empty(); $(this).append("<img style='vertical-align:middle;'
border='0' src='/images/ic_menu_pause.png'/>"
); current = this; return false; });
Works great!
Technorati Tags:
VSAchievements
Visual Studio Achievements
Karsten Januszewski (207 Points)