<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss2full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0">

<channel>
	<title>Dimebrain</title>
	
	<link>http://dimebrain.com</link>
	<description>Social software in .NET</description>
	<lastBuildDate>Sun, 08 Nov 2009 05:28:09 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.8.5</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<image><link>www.dimebrain.com</link><url>http://dimebrain.com/wp-content/themes/dimebrain/screenshot.png</url><title>dimebrain</title></image><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" href="http://feeds.feedburner.com/Dimebrain" type="application/rss+xml" /><feedburner:emailServiceId>Dimebrain</feedburner:emailServiceId><feedburner:feedburnerHostname>http://feedburner.google.com</feedburner:feedburnerHostname><feedburner:feedFlare href="http://add.my.yahoo.com/rss?url=http%3A%2F%2Ffeeds.feedburner.com%2FDimebrain" src="http://us.i1.yimg.com/us.yimg.com/i/us/my/addtomyyahoo4.gif">Subscribe with My Yahoo!</feedburner:feedFlare><feedburner:feedFlare href="http://www.newsgator.com/ngs/subscriber/subext.aspx?url=http%3A%2F%2Ffeeds.feedburner.com%2FDimebrain" src="http://www.newsgator.com/images/ngsub1.gif">Subscribe with NewsGator</feedburner:feedFlare><feedburner:feedFlare href="http://feeds.my.aol.com/add.jsp?url=http%3A%2F%2Ffeeds.feedburner.com%2FDimebrain" src="http://o.aolcdn.com/favorites.my.aol.com/webmaster/ffclient/webroot/locale/en-US/images/myAOLButtonSmall.gif">Subscribe with My AOL</feedburner:feedFlare><feedburner:feedFlare href="http://www.bloglines.com/sub/http://feeds.feedburner.com/Dimebrain" src="http://www.bloglines.com/images/sub_modern11.gif">Subscribe with Bloglines</feedburner:feedFlare><feedburner:feedFlare href="http://www.netvibes.com/subscribe.php?url=http%3A%2F%2Ffeeds.feedburner.com%2FDimebrain" src="http://www.netvibes.com/img/add2netvibes.gif">Subscribe with Netvibes</feedburner:feedFlare><feedburner:feedFlare href="http://fusion.google.com/add?feedurl=http%3A%2F%2Ffeeds.feedburner.com%2FDimebrain" src="http://buttons.googlesyndication.com/fusion/add.gif">Subscribe with Google</feedburner:feedFlare><feedburner:feedFlare href="http://www.pageflakes.com/subscribe.aspx?url=http%3A%2F%2Ffeeds.feedburner.com%2FDimebrain" src="http://www.pageflakes.com/ImageFile.ashx?instanceId=Static_4&amp;fileName=ATP_blu_91x17.gif">Subscribe with Pageflakes</feedburner:feedFlare><feedburner:browserFriendly>Hi, thanks for thinking this content is worth keeping an eye on. That's a lovely sweater you're wearing.</feedburner:browserFriendly><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><item>
		<title>A theming engine for ASP.NET MVC 2</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/jx0jwpk5Bqo/a-themes-engine-for-asp-net-mvc-2.html</link>
		<comments>http://dimebrain.com/2009/11/a-themes-engine-for-asp-net-mvc-2.html#comments</comments>
		<pubDate>Fri, 06 Nov 2009 21:47:53 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[asp.net mvc]]></category>
		<category><![CDATA[themes]]></category>

		<guid isPermaLink="false">http://dimebrain.com/?p=745</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F11%2Fa-themes-engine-for-asp-net-mvc-2.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F11%2Fa-themes-engine-for-asp-net-mvc-2.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;
Get the example code shown in the video &lt;a href="http://dimebrain.com.s3.amazonaws.com/MvcTheming.zip" title="MVCTheming.zip"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;ve wanted a way to apply custom themes in ASP.NET MVC that &amp;#8220;just works&amp;#8221; for awhile. Sometimes you want to apply a new theme that switches out just a few CSS elements, or changes the structure of a specific view, or the entire site. There&amp;#8217;s no out-of-the-box way to do that, and the existing solutions I&amp;#8217;ve seen will only solve the CSS problem, or the views problem, but not both, or they require you to duplicate your views for every theme or use exact file names rather than your own, which&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F11%2Fa-themes-engine-for-asp-net-mvc-2.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F11%2Fa-themes-engine-for-asp-net-mvc-2.html" height="61" width="51" /></a></div><p><br/><br />
Get the example code shown in the video <a href="http://dimebrain.com.s3.amazonaws.com/MvcTheming.zip" title="MVCTheming.zip">here</a>.</p>
<p>I&#8217;ve wanted a way to apply custom themes in ASP.NET MVC that &#8220;just works&#8221; for awhile. Sometimes you want to apply a new theme that switches out just a few CSS elements, or changes the structure of a specific view, or the entire site. There&#8217;s no out-of-the-box way to do that, and the existing solutions I&#8217;ve seen will only solve the CSS problem, or the views problem, but not both, or they require you to duplicate your views for every theme or use exact file names rather than your own, which is a maintenance nightmare.</p>
<p>I&#8217;ve come up with a ThemeViewEngine, based on the current WebFormViewEngine, that allows you to opt-in to only the changes you need, whether that&#8217;s a CSS file there, a MasterPage here, a couple of views, etc. Since it&#8217;s written for ASP.NET MVC 2, it supports themes within areas as well. The next listing isn&#8217;t the whole engine, but it highlights the important points, which are the locations where theme content lives, and finding the theme in the current session or query string. In production, it&#8217;s not a great idea to use cookies (or use the URI for cookieless sessions), so you&#8217;ll probably want to tie the theme to a user&#8217;s session in a different way.</p>
<pre class="brush:csharp">
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;

namespace MvcTheming
{
    /// <summary>
    /// Provides a flexible <see cref="WebFormViewEngine" /> for adding theming capabilities to your views.
    /// You have the option of using a theme to override only what you need, whether that's CSS, Javascript,
    /// MasterPages, or specific views. This way both look and feel as well as site structure may be
    /// changed on demand.
    /// </summary>
    public class ThemeViewEngine : WebFormViewEngine
    {
        // format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:{themeName}"
        private const string CacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}";
        private const string CacheKeyPrefixMaster = "Master";
        private const string CacheKeyPrefixPartial = "Partial";
        private const string CacheKeyPrefixView = "View";

        private static readonly string[] _emptyLocations = new string[0];

        public string Theme { get; private set; }

        // {0}name:{1}controllerName:{2}areaName:{3}themeName
        public ThemeViewEngine()
        {
            MasterLocationFormats = new[] {
                                              // Theme-specific locations (opt-in)
                                              "~/Content/{3}/{0}.master",
                                              "~/Content/{3}/Views/{1}/{0}.master",
                                              "~/Content/{3}/Views/Shared/{0}.master",
                                              // Default locations
                                              "~/Views/{1}/{0}.master",
                                              "~/Views/Shared/{0}.master"
                                          };

            AreaMasterLocationFormats = new[] {
                                                  // Theme-specific locations (opt-in)
                                                  "~/Areas/{2}/Content/{3}/{0}.master",
                                                  "~/Areas/{2}/Content/{3}/Views/{1}/{0}.master",
                                                  "~/Areas/{2}/Content/{3}/Views/Shared/{0}.master",
                                                  "~/Content/{3}/Areas/{2}/{0}.master}",
                                                  "~/Content/{3}/Areas/{2}/Views/{1}/{0}.master}",
                                                  "~/Content/{3}/Areas/{2}/Views/Shared/{0}.master}",
                                                  // Default locations
                                                  "~/Areas/{2}/Views/{1}/{0}.master",
                                                  "~/Areas/{2}/Views/Shared/{0}.master",
                                                  "~/Views/Areas/{2}/{1}/{0}.master",
                                                  "~/Views/Areas/{2}/Shared/{0}.master"
                                              };

            ViewLocationFormats = new[] {
                                            // Theme-specific locations (opt-in)
                                            "~/Content/{3}/Views/{1}/{0}.aspx",
                                            "~/Content/{3}/Views/{1}/{0}.ascx",
                                            "~/Content/{3}/Views/Shared/{0}.aspx",
                                            "~/Content/{3}/Views/Shared/{0}.ascx",
                                            // Default locations
                                            "~/Views/{1}/{0}.aspx",
                                            "~/Views/{1}/{0}.ascx",
                                            "~/Views/Shared/{0}.aspx",
                                            "~/Views/Shared/{0}.ascx"
                                        };

            AreaViewLocationFormats = new[] {
                                                // Theme-specific locations (opt-in)
                                                "~/Areas/{2}/Content/{3}/Views/{1}/{0}.aspx",
                                                "~/Areas/{2}/Content/{3}/Views/{1}/{0}.ascx",
                                                "~/Areas/{2}/Content/{3}/Views/Shared/{0}.aspx",
                                                "~/Areas/{2}/Content/{3}/Views/Shared/{0}.ascx",
                                                "~/Content/{3}/Views/Areas/{2}/{1}/{0}.aspx",
                                                "~/Content/{3}/Views/Areas/{2}/{1}/{0}.ascx",
                                                "~/Content/{3}/Views/Areas/{2}/Shared/{0}.aspx",
                                                "~/Content/{3}/Views/Areas/{2}/Shared/{0}.ascx",
                                                // Default locations
                                                "~/Areas/{2}/Views/{1}/{0}.aspx",
                                                "~/Areas/{2}/Views/{1}/{0}.ascx",
                                                "~/Areas/{2}/Views/Shared/{0}.aspx",
                                                "~/Areas/{2}/Views/Shared/{0}.ascx",
                                                "~/Views/Areas/{2}/{1}/{0}.aspx",
                                                "~/Views/Areas/{2}/{1}/{0}.ascx",
                                                "~/Views/Areas/{2}/Shared/{0}.aspx",
                                                "~/Views/Areas/{2}/Shared/{0}.ascx"
                                            };

            PartialViewLocationFormats = ViewLocationFormats;
            AreaPartialViewLocationFormats = AreaViewLocationFormats;
        }

        // Details elided. I like the word "elided". Thanks Bill Wagner.

        private void SetTheme(ControllerContext controllerContext)
        {
            var context = controllerContext.RequestContext.HttpContext;
            var request = context.Request;
            var session = context.Session;
            var queryString = request.QueryString;

            if (queryString.AllKeys.Contains("theme"))
            {
                var theme = queryString["theme"];
                session.Add("Theme", theme);
            }

            if (session["Theme"] == null)
            {
                return;
            }

            Theme = session["Theme"].ToString();
        }

        // Details elided

        private class ViewLocation
        {
            protected readonly string _virtualPathFormatString;

            public ViewLocation(string virtualPathFormatString)
            {
                _virtualPathFormatString = virtualPathFormatString;
            }

            public virtual string Format(string viewName, string controllerName, string areaName, string themeName)
            {
                var result = String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName, themeName);
                return result;
            }
        }

        private class AreaAwareViewLocation : ViewLocation
        {
            public AreaAwareViewLocation(string virtualPathFormatString)
                : base(virtualPathFormatString)
            {

            }

            public override string Format(string viewName, string controllerName, string areaName, string themeName)
            {
                var result = String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName, themeName);
                return result;
            }
        }
    }
}
</pre>
<p>Once you have a theme engine capable of looking for the right files in the right places, a set of extension methods is used to provide a way to inject &#8220;soft references&#8221; into your master pages and views, so that your default site design can flex when the theme changes, rather than point to the default content regardless of a new theme. </p>
<pre class="brush:csharp">
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;

namespace MvcTheming
{
    public static class ThemeExtensions
    {
        // format is ":ThemeCacheEntry:{areaName}:{themeName}:{resourceType}:{resourceName}"
        private const string CacheKeyFormat = ":ThemeCacheEntry:{0}:{1}:{2}:{3}";

        // {0}areaName:{1}themeName:{2}resourceType:{3}resourceName
        private static readonly string[] _contentLocationFormats = new[]
           {
               // Area and type-specific locations
               "~/Areas/{0}/Content/{1}/{3}",
               "~/Areas/{0}/Content/{2}/{3}",
               "~/Areas/{0}/Content/{1}/{2}/{3}",
               "~/Areas/{0}/Content/{1}/{2}/{3}",
               // Area-specific default locations
               "~/Areas/{0}/Content/{3}",
               "~/Areas/{0}/Scripts/{3}",
               // Area default locations
               "~/Areas/Content/{3}",
               "~/Areas/Scripts/{3}",
               // Theme and type-specific locations
               "~/Content/{1}/{3}",
               "~/Content/{2}/{3}",
               "~/Content/{1}/{2}/{3}",
               "~/Scripts/{1}/{3}",
               // Default locations
               "~/Content/{3}",
               "~/Scripts/{3}",
           };

        public static string JavaScript(this UrlHelper helper, string scriptName)
        {
            return StaticResource(helper, "JavaScript", scriptName);
        }

        public static string Css(this UrlHelper helper, string styleName)
        {
            return StaticResource(helper, "Css", styleName);
        }

        public static string Image(this UrlHelper helper, string imageName)
        {
            return StaticResource(helper, "Images", imageName);
        }

        public static string StaticResource(this UrlHelper helper,
                                            string resourceType,
                                            string resourceName)
        {
            return helper.StaticResource(resourceType, resourceName, true);
        }

        public static string StaticResource(this UrlHelper helper,
                                            string resourceType,
                                            string resourceName,
                                            bool useCache)
        {
            var areaName = helper.RequestContext.RouteData.GetAreaName();
            var themeName = FindThemeName();

            var cacheKey = String.Format(CacheKeyFormat, areaName, themeName, resourceType, resourceName);
            if(useCache)
            {
                if(HttpRuntime.Cache[cacheKey] != null)
                {
                    return HttpRuntime.Cache[cacheKey].ToString();
                }
            }

            var searchedLocations = new List<string>();
            foreach (var mask in _contentLocationFormats)
            {
                var relativePath = String.Format(mask, areaName, themeName ?? "", resourceType, resourceName);
                var absolutePath = VirtualPathUtility.ToAbsolute(relativePath);
                var serverPath = helper.RequestContext.HttpContext.Server.MapPath(absolutePath);

                searchedLocations.Add(absolutePath);
                if (!File.Exists(serverPath))
                {
                    continue;
                }

                HttpRuntime.Cache.Insert(cacheKey, absolutePath, new CacheDependency(serverPath));
                return absolutePath;
            }

            throw new FileNotFoundException();
        }

        private static string FindThemeName()
        {
            string themeName = null;
            foreach(var engine in ViewEngines.Engines)
            {
                var themeViewEngine = engine as ThemeViewEngine;
                if (themeViewEngine == null)
                {
                    continue;
                }

                themeName = themeViewEngine.Theme;
            }
            return themeName;
        }
    }
}
</pre>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~ff/Dimebrain?a=jx0jwpk5Bqo:8jhs4hLJDMA:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/Dimebrain?i=jx0jwpk5Bqo:8jhs4hLJDMA:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=jx0jwpk5Bqo:8jhs4hLJDMA:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/Dimebrain?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=jx0jwpk5Bqo:8jhs4hLJDMA:gIN9vFwOqvQ"><img src="http://feeds.feedburner.com/~ff/Dimebrain?i=jx0jwpk5Bqo:8jhs4hLJDMA:gIN9vFwOqvQ" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=jx0jwpk5Bqo:8jhs4hLJDMA:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/Dimebrain?d=qj6IDK7rITs" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/jx0jwpk5Bqo" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/11/a-themes-engine-for-asp-net-mvc-2.html/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/11/a-themes-engine-for-asp-net-mvc-2.html</feedburner:origLink></item>
		<item>
		<title>Using a controller to manage static resources</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/KB__fNIcwLs/using-a-controller-to-manage-static-resources.html</link>
		<comments>http://dimebrain.com/2009/07/using-a-controller-to-manage-static-resources.html#comments</comments>
		<pubDate>Mon, 13 Jul 2009 13:57:59 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[asp.net mvc]]></category>
		<category><![CDATA[controllers]]></category>
		<category><![CDATA[static resources]]></category>

		<guid isPermaLink="false">http://dimebrain.com/?p=714</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F07%2Fusing-a-controller-to-manage-static-resources.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F07%2Fusing-a-controller-to-manage-static-resources.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;If you&amp;#8217;re like me, you&amp;#8217;re more or less addicted to &lt;a title="YSlow" href="http://http://developer.yahoo.com/yslow/" target="_blank"&gt;YSlow&lt;/a&gt; and getting the best performance for your web applications whenever you can. One challenge is to address multiple YSlow recommendations at the same time, particularly when it asks you to compress the content sent to clients, and in the same breath asks that ETags are used to avoid resending cached content for particular resources. In &lt;a href="http://www.mnot.net/blog/2007/08/07/etags"&gt;this excellent overview&lt;/a&gt; of some of the pitfalls of using ETags, you&amp;#8217;ll learn that if you try to base your ETag value on compressed content, the timestamp-based format of that content will cause a new&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F07%2Fusing-a-controller-to-manage-static-resources.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F07%2Fusing-a-controller-to-manage-static-resources.html" height="61" width="51" /></a></div><p>If you&#8217;re like me, you&#8217;re more or less addicted to <a title="YSlow" href="http://http://developer.yahoo.com/yslow/" target="_blank">YSlow</a> and getting the best performance for your web applications whenever you can. One challenge is to address multiple YSlow recommendations at the same time, particularly when it asks you to compress the content sent to clients, and in the same breath asks that ETags are used to avoid resending cached content for particular resources. In <a href="http://www.mnot.net/blog/2007/08/07/etags">this excellent overview</a> of some of the pitfalls of using ETags, you&#8217;ll learn that if you try to base your ETag value on compressed content, the timestamp-based format of that content will cause a new ETag to generate for every resource request, and the end result is a constant fetch of the resource, breaking your caching scheme.</p>
<p>For this challenge I decided to use a Controller, and mapped its route so that a request for the Site.css file in my &#8220;/Content/&#8221; folder, where the default project template stores CSS files (and where I store all static content), is declared in markup as &#8220;/static/Site.css&#8221;. Not a big difference for me, but I can customize the controller&#8217;s behavior later to search in multiple places, hiding the details behind the static URL path.</p>
<p>In ASP.NET MVC you have a few options for getting between the pipeline to fine tune your requests and responses. In this post I want to outline a static resource controller I&#8217;m using to ensure that my static content is cached on both the client and the server, is sent compressed whenever possible, and makes appropriate use of ETags. Your static resource controller should do the following things for you:</p>
<ul>
<li>It should cache content on the client, and know when to send a 304 &#8211; Not Modified whenever the request is asking for an ETag that maps to the file it&#8217;s looking for, or if the last modified date of the resource file hasn&#8217;t changed since the last request. I prefer to evaluate the ETag first, just in case the file&#8217;s last modified date was changed, but the content of the file itself is identical, since that would result in an unnecessary cache invalidation.</li>
<li>It should cache content on the server, ensuring multiple requests from <em>different</em> browsers are handled efficiently (in this case from memory rather than from disk), and know when to invalidate the cached copy as soon as the underlying resource changes. This will help when you are lucky enough to have a traffic problem.</li>
<li>It should compress the content returned to the client based on what the client is able is to accept.</li>
<li>It should work with GZIP and DEFLATE, and the ETags should take these into account so that the controller works even when compression is turned off.</li>
</ul>
<p>Here&#8217;s the skeleton version of the controller, with a few missing methods, but enough to read through to see how an HTTP GET call using the controller handles static resources.</p>
<pre class="brush:csharp">
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;
using Dimebrain.Commons.Mvc.Extensions;
using Dimebrain.Commons.Mvc.Filters;

namespace Dimebrain.Commons.Mvc.Controllers
{
    [CompressFilter]
    public class StaticResourceController : Controller
    {
        [AcceptVerbs(HttpVerbs.Get)]
        public void Get(string file)
        {
            var relativePath = string.Format("~/Content/{0}", file);
            var absolutePath = Server.MapPath(relativePath);

            // 304 (If-None-Match), a better test of uniqueness than modified date
            var etag = GenerateETag(absolutePath);
            if (BrowserIsRequestingFileIdentifiedBy(etag))
            {
                Response.StatusCode = (int)HttpStatusCode.NotModified;
                Response.SuppressContent = true;
                return;
            }

            // 404 (NotFound), if ETag differs, I/O access is inevitable
            if (!System.IO.File.Exists(absolutePath))
            {
                Response.StatusCode = (int)HttpStatusCode.NotFound;
                Response.SuppressContent = true;
                return;
            }

            // 304 (If-Last-Modified)
            var lastModified = new FileInfo(absolutePath).LastWriteTime;
            if(BrowserIsRequestingFileUnmodifiedSince(lastModified))
            {
                Response.StatusCode = (int) HttpStatusCode.NotModified;
                Response.SuppressContent = true;
                return;
            }

            // 200 - OK
            CacheOnClient(etag, lastModified);
            var content = CacheOrFetchFromServer(relativePath, absolutePath);
            using (var sw = new StreamWriter(Response.OutputStream))
            {
                sw.Write(content);
            }

            var contentType = absolutePath.MimeType();
            Response.ContentType = contentType;
        }
    }
}
</pre>
<p>That&#8217;s the controller in a nutshell. It compresses the stream serving the content, and it gives the browser multiple opportunities to use its cached copy, and it also caches the content on the server. Looking closer at the supporting methods of the controller, you&#8217;ll find that both the ETag and the server cache dependency on based on the file itself; when it changes, a new value is generated for the ETag, and the server cache is invalidated. This is a great way to set this controller up and forget about it, because whenever you make a change to the underlying resource, all bets are off and the browser is guaranteed to get the latest version on the next request.</p>
<pre class="brush:csharp">
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;
using Dimebrain.Commons.Mvc.Extensions;
using Dimebrain.Commons.Mvc.Filters;

namespace Dimebrain.Commons.Mvc.Controllers
{
    [CompressFilter]
    public class StaticResourceController : Controller
    {
        private const string DeflateTag = "-deflate";
        private const string GzipTag = "-gzip";
        private const string LastModifiedSinceHeader = "If-Modified-Since";
        private const string IfNoneMatchHeader = "If-None-Match";
        private const string DefaultEncodingCodePage = "iso-8859-1";

        [AcceptVerbs(HttpVerbs.Get)]
        public void Get(string file)
        {
            var relativePath = string.Format("~/Content/{0}", file);
            var absolutePath = Server.MapPath(relativePath);

            // 304 (If-None-Match), a better test of uniqueness than modified date
            var etag = GenerateETag(absolutePath);
            if (BrowserIsRequestingFileIdentifiedBy(etag))
            {
                Response.StatusCode = (int)HttpStatusCode.NotModified;
                Response.SuppressContent = true;
                return;
            }

            // 404 (NotFound)
            if (!System.IO.File.Exists(absolutePath))
            {
                Response.StatusCode = (int)HttpStatusCode.NotFound;
                Response.SuppressContent = true;
                return;
            }

            // 304 (If-Last-Modified)
            var lastModified = new FileInfo(absolutePath).LastWriteTime;
            if(BrowserIsRequestingFileUnmodifiedSince(lastModified))
            {
                Response.StatusCode = (int) HttpStatusCode.NotModified;
                Response.SuppressContent = true;
                return;
            }

            // 200 - OK
            CacheOnClient(etag, lastModified);
            var content = CacheOrFetchFromServer(relativePath, absolutePath);
            using (var sw = new StreamWriter(Response.OutputStream))
            {
                sw.Write(content);
            }

            var contentType = absolutePath.MimeType();
            Response.ContentType = contentType;
        }

        private bool BrowserIsRequestingFileUnmodifiedSince(DateTime lastModified)
        {
            if (Request.Headers[LastModifiedSinceHeader] == null)
            {
                return false;
            }

            // Header values may have additional attributes separated by semi-colons
            var ifModifiedSince = Request.Headers[LastModifiedSinceHeader];
            if (ifModifiedSince.IndexOf(";") > -1)
            {
                ifModifiedSince = ifModifiedSince.Split(';').First();
            }

            // Get the dates for comparison; truncate milliseconds in date if needed
            var sinceDate = Convert.ToDateTime(ifModifiedSince).ToUniversalTime();
            var fileDate = lastModified.ToUniversalTime();
            if (sinceDate.Millisecond.Equals(0))
            {
                fileDate = new DateTime(fileDate.Year,
                                        fileDate.Month,
                                        fileDate.Day,
                                        fileDate.Hour,
                                        fileDate.Minute,
                                        fileDate.Second,
                                        0);
            }

            return fileDate.BeforeOrEqual(sinceDate);
        }

        private bool BrowserIsRequestingFileIdentifiedBy(string etag)
        {
            if (Request.Headers[IfNoneMatchHeader] == null)
            {
                return false;
            }

            var ifNoneMatch = Request.Headers[IfNoneMatchHeader];
            return ifNoneMatch.EqualsIgnoreCase(etag);
        }

        private static string CacheOrFetchFromServer(string relativePath, string absolutePath)
        {
            var cache = HttpRuntime.Cache;
            string content;
            if (cache[relativePath] == null)
            {
                var encoding = Encoding.GetEncoding(DefaultEncodingCodePage);
                var dependency = new CacheDependency(absolutePath);

                content = System.IO.File.ReadAllText(absolutePath, encoding);
                cache.Insert(relativePath, content, dependency);
            }
            else
            {
                content = cache[relativePath].ToString();
            }

            return content;
        }

        private bool HasGzipFilter
        {
            // You can't inspect the response headers directly in IIS6 / Cassini
            get { return Convert.ToBoolean(HttpContext.Items["gzipped"] ?? "false"); }
        }

        private bool HasDeflateFilter
        {
            // You can't inspect the response headers directly in IIS6 / Cassini
            get { return Convert.ToBoolean(HttpContext.Items["deflated"] ?? "false"); }
        }

        private void CacheOnClient(string etag, DateTime lastModified)
        {
            var futureDate = 365.Days();
            var expires = futureDate.FromNow();
            var cache = Response.Cache;

            // Cacheability must be set to public for SetETag to work; you could also
            // add the ETag header yourself with AppendHeader or AddHeader methods
            cache.SetCacheability(HttpCacheability.Public);
            cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
            cache.SetExpires(expires);
            cache.SetMaxAge(futureDate);
            cache.SetLastModified(lastModified);
            cache.SetETag(etag);
        }

        private string GenerateETag(string absolutePath)
        {
            var sb = new StringBuilder();
            sb.Append(absolutePath.MD5());

            if(HasGzipFilter)
            {
                sb.Append(GzipTag);
            }
            else if(HasDeflateFilter)
            {
                sb.Append(DeflateTag);
            }
            return sb.ToString();
        }
    }
}
</pre>
<p>You can see how the Cassini workaround is used in the code above, to check if either GZIP or DEFLATE was used based on the results of compression. If it was, the ETag is appended with the appropriate value, so that a browser that doesn&#8217;t support GZIP (and will rename nameless to protect the guilty) can still participate in ETags but not accidentally receive a compressed version, which would happen if only the MD5 was used to identify the file. You&#8217;re also throwing everything but the kitchen sink at the client cache, giving it more than enough information to work with caching. Keep in mind that it&#8217;s still up to your code to handle telling the browser that there&#8217;s nothing to see here, and that it&#8217;s time to move along, by comparing &#8220;If-Modified-Since&#8221; against the resource&#8217;s last modified date, and &#8220;If-None-Match&#8221; against the current ETag generated for the resource, to see if there are any discrepancies. There&#8217;s a few gotchas in this process, such as ensuring you truncate milliseconds in your file&#8217;s last modified timestamp, if the browser does the same thing, otherwise you&#8217;ll continue to send the same cached file every time.</p>
<p>To set up compression, you can use an <code>ActionFilterAttribute</code>, overriding the <code>OnActionExecuting </code>event to spin up a compression filter and add it to the output stream. The reason you would run compression prior to the action completing in this case is because I personally prefer to use the Visual Studio Development Server, aka Cassini, when debugging my applications. Unfortunately, Cassini emulates II6 which does not support integrated pipeline mode, and will cause exceptions when you try to inspect response headers directly when asking questions like &#8220;Is this response compressed?&#8221;, so, I apply compression before my controller action executes, and toss the results of the compression filter into <code>HttpContext.Items</code> so it&#8217;s available for the action down the line. You don&#8217;t have to do this if you&#8217;re debugging on a local IIS7 instance, or if you&#8217;re comfortable <a href="http://www.youtube.com/watch?v=2tJjNVVwRCY">doing it live</a>. Here&#8217;s the code to add compression to the controller through a filter.</p>
<pre class="brush:csharp">
using System.IO.Compression;
using System.Web.Mvc;

namespace Dimebrain.Commons.Mvc.Filters
{
    public class CompressFilterAttribute : ActionFilterAttribute
    {
        private const string ContentEncodingHeader = "Content-Encoding";
        private const string AcceptEncodingHeader = "Accept-Encoding";
        private const string GZipValue = "gzip";
        private const string DeflateValue = "deflate";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.HttpContext.Request;
            var acceptEncoding = request.Headers[AcceptEncodingHeader];

            if (string.IsNullOrEmpty(acceptEncoding)) return;

            acceptEncoding = acceptEncoding.ToLowerInvariant();
            var response = filterContext.HttpContext.Response;

            if (acceptEncoding.Contains(GZipValue))
            {
                response.AppendHeader(ContentEncodingHeader, GZipValue);
                response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);

                // Hack so we can continue to use Cassini
                filterContext.HttpContext.Items["gzipped"] = "true";
            }
            else if (acceptEncoding.Contains(DeflateValue))
            {
                response.AppendHeader(ContentEncodingHeader, DeflateValue);
                response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);

                // Hack so we can continue to use Cassini
                filterContext.HttpContext.Items["deflated"] = "true";
            }
        }
    }
}
</pre>
<p>The last thing you need to do to get this working is declare the controller route wherever you&#8217;re doing such things, which by default is in your <code>Global.asax</code> file&#8217;s <code>Application_Start</code> event handler (via the <code>RegisterRoutes </code>method).</p>
<pre class="brush:csharp">
// Static Resources
routes.Add(new Route
    ("static/{file}", new MvcRouteHandler())
    {
        Defaults = new RouteValueDictionary(
                   new { controller = "StaticResource",
                         action = "Get",
                         file = ""
                       })
    });
</pre>
<p>Now you can declare your static resources that live in the Content folder like this, and get all the benefits of the controller.</p>
<pre class="brush:xml">
<head runat="server">
<link href="/static/Site.css" rel="stylesheet" type="text/css" //>
</head>
</pre>
<p>That&#8217;s the code. The last snippet shows you all of the extension methods I used to make the example more readable. Hopefully this will feed your YSlow addiction just a little bit more, but more importantly, work effectively with cached static content in your MVC projects.</p>
<pre class="brush:csharp">
public static string MD5(this string filePath)
{
    var sb = new StringBuilder();
    using (var fs = new FileStream(filePath, FileMode.Open))
    {
        using (var br = new BinaryReader(fs))
        {
            var md5 = new MD5CryptoServiceProvider();
            var hash = md5.ComputeHash(br.BaseStream);
            foreach (var hex in hash)
            {
                sb.Append(hex.ToString("x2"));
            }

            return sb.ToString();
        }
    }
}

public static string MimeType(this string filePath)
{
    const string key = "Content Type";

    var extension = Path.GetExtension(filePath).ToLowerInvariant();
    var registryKey = Registry.ClassesRoot.OpenSubKey(extension);

    return registryKey != null &#038;&#038; registryKey.GetValue(key) != null
               ? registryKey.GetValue(key).ToString()
               : "application/octet-stream";
}

public static bool EqualsIgnoreCase(this string left, string right)
{
    return String.Compare(left, right, true) == 0;
}

public static TimeSpan Days(this int value)
{
    return TimeSpan.FromDays(value);
}

public static DateTime FromNow(this TimeSpan value)
{
    return DateTime.UtcNow.Add(value);
}

public static bool BeforeOrEqual(this DateTime left, DateTime right)
{
    return left.CompareTo(right) <= 0;
}
</pre>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~ff/Dimebrain?a=KB__fNIcwLs:TAQOvCDTlbQ:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/Dimebrain?i=KB__fNIcwLs:TAQOvCDTlbQ:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=KB__fNIcwLs:TAQOvCDTlbQ:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/Dimebrain?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=KB__fNIcwLs:TAQOvCDTlbQ:gIN9vFwOqvQ"><img src="http://feeds.feedburner.com/~ff/Dimebrain?i=KB__fNIcwLs:TAQOvCDTlbQ:gIN9vFwOqvQ" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=KB__fNIcwLs:TAQOvCDTlbQ:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/Dimebrain?d=qj6IDK7rITs" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/KB__fNIcwLs" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/07/using-a-controller-to-manage-static-resources.html/feed</wfw:commentRss>
		<slash:comments>6</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/07/using-a-controller-to-manage-static-resources.html</feedburner:origLink></item>
		<item>
		<title>Why Twitter clients don’t matter</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/Gl1cuqbUotg/why-twitter-clients-dont-matter.html</link>
		<comments>http://dimebrain.com/2009/04/why-twitter-clients-dont-matter.html#comments</comments>
		<pubDate>Sat, 18 Apr 2009 15:08:20 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Conversations]]></category>
		<category><![CDATA[clients]]></category>
		<category><![CDATA[twitter]]></category>

		<guid isPermaLink="false">http://dimebrain.com/?p=607</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F04%2Fwhy-twitter-clients-dont-matter.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F04%2Fwhy-twitter-clients-dont-matter.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Since creating &lt;a href="http://tweetsharp.com" target="_blank"&gt;TweetSharp&lt;/a&gt;, I&amp;#8217;ve had the opportunity to speak with a lot of folks who are using it to build their Twitter projects, which is great to hear. Most developers feel that a) the current state of the union for Twitter clients like &lt;a href="http://desktop.seesmic.com/"&gt;Seesmic Desktop&lt;/a&gt; and &lt;a href="http://www.twhirl.org/" target="_blank"&gt;Twhirl&lt;/a&gt;, &lt;a href="http://www.tweetdeck.com"&gt;TweetDeck &lt;/a&gt;and &lt;a href="http://hootsuite.com/"&gt;hootsuite&lt;/a&gt;, &lt;a href="http://www.thirteen23.com/"&gt;blu &lt;/a&gt;and &lt;a href="http://wittytwitter.googlecode.com" target="_blank"&gt;Witty&lt;/a&gt;, just doesn&amp;#8217;t do &amp;#8220;all of the things I&amp;#8217;d want a Twitter client to do&amp;#8221;. And this is fair criticism, since client developers can&amp;#8217;t possibly anticipate every use for Twitter, and new uses are emerging all the time.&lt;/p&gt;
&lt;p&gt;The most common use for &lt;a href="http://tweetsharp.com" target="_blank"&gt;TweetSharp&lt;/a&gt; has been to accelerate the development&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F04%2Fwhy-twitter-clients-dont-matter.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F04%2Fwhy-twitter-clients-dont-matter.html" height="61" width="51" /></a></div><p><object width="400" height="342"><param name="movie" value="http://current.com/e/89891774/en_GB"></param><param name="wmode" value="transparent"></param><param name="allowfullscreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://current.com/e/89891774/en_GB" type="application/x-shockwave-flash"  width="400" height="342" wmode="transparent" allowfullscreen="true" allowscriptaccess="always"></embed></object></p>
<p>Since creating <a href="http://tweetsharp.com" target="_blank">TweetSharp</a>, I&#8217;ve had the opportunity to speak with a lot of folks who are using it to build their Twitter projects, which is great to hear. Most developers feel that a) the current state of the union for Twitter clients like <a href="http://desktop.seesmic.com/">Seesmic Desktop</a> and <a href="http://www.twhirl.org/" target="_blank">Twhirl</a>, <a href="http://www.tweetdeck.com">TweetDeck </a>and <a href="http://hootsuite.com/">hootsuite</a>, <a href="http://www.thirteen23.com/">blu </a>and <a href="http://wittytwitter.googlecode.com" target="_blank">Witty</a>, just doesn&#8217;t do &#8220;all of the things I&#8217;d want a Twitter client to do&#8221;. And this is fair criticism, since client developers can&#8217;t possibly anticipate every use for Twitter, and new uses are emerging all the time.</p>
<p>The most common use for <a href="http://tweetsharp.com" target="_blank">TweetSharp</a> has been to accelerate the development of Twitter clients, which makes sense considering that is exactly why it was written in the first place. It is the API running the show behind the scenes of our own client. After working hard over the last while on our next-generation Twitter client, and watching the community conversations about taking things to the next level, we&#8217;ve realized: <em>Twitter clients don&#8217;t matter, applications do</em>. Here&#8217;s why:</p>
<ul>
<li><strong>Too many choices</strong>. I have a pet prediction that there will be around thirty choices of Twitter client around the summer of 2009. I know of several development teams working under a veil on their version of client revolution, myself and <a href="http://twitter.com/jdiller" target="_blank">@jdiller </a>included, that plan to launch this summer. This is great for users, the more options the better, but usually when faced with overwhelming alternatives, people stick with the default. That means more TweetDeck users, and more developers with superior offerings left scratching their heads.</li>
</ul>
<ul>
<li><strong>Lack of diversity</strong>. The next evolution of Twitter clients will ultimately need to make journeys into extensibility; it just makes sense. Features like groups that persist across devices, interchanging multi-column and condensed views, slick UX, offline support, are not innovations when every developer who builds a client knows those features are necessary and are part and parcel of good software: this is the minimum bar. That means having a client that can load plugins to extend features, and possibly opening up that plugin model for others to build on top of your client, are going to be part of just about every competitive Twitter client going. TweetDeck will bring these features in if they aren&#8217;t working on them already. That means more TweetDeck users, and more developers with superior offerings left scratching their heads.</li>
<li><strong>The sunset of &#8220;features-as-applications</strong>&#8221; . Once the vast host of Twitter clients converge on the same basic set of features and offer a decent extensibility model, the game will change to swallowing up all of the applications that have managed to attract a following simply because they fulfill one missing feature of Twitter&#8217;s default offering. Want to know who&#8217;s stopped following you? That&#8217;s <a href="http://useqwitter.com" target="_blank">Qwitter</a>. Want to broadcast tweets at certain times to pre-defined groups? That&#8217;s <a href="http://tweetlater.com" target="_blank">TweetLater</a>. And the list goes on, but all of these applications are not true applications, <em>they&#8217;re just features</em>. Features are not a good reason to build a business. The Twitter client army will devour these features as either native functions of the client itself, or bring them in piecemeal as plugins.</li>
</ul>
<p>What&#8217;s left are applications in the true sense of the word: unique software that solves a particular need, whether broad or niche. This category is as wide open as innovation itself. You&#8217;ve always had the option of doing something remarkable.</p>
<p>Want to know how to get users to switch from TweetDeck to your client? Build an application worth switching clients for, then offer it exclusively from within yours. All things being equal&#8211;and they will be sooner than you think&#8211;nobody is loyal to their Twitter client.</p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~ff/Dimebrain?a=Gl1cuqbUotg:q5sAbbEMiW8:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/Dimebrain?i=Gl1cuqbUotg:q5sAbbEMiW8:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=Gl1cuqbUotg:q5sAbbEMiW8:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/Dimebrain?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=Gl1cuqbUotg:q5sAbbEMiW8:gIN9vFwOqvQ"><img src="http://feeds.feedburner.com/~ff/Dimebrain?i=Gl1cuqbUotg:q5sAbbEMiW8:gIN9vFwOqvQ" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/Dimebrain?a=Gl1cuqbUotg:q5sAbbEMiW8:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/Dimebrain?d=qj6IDK7rITs" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/Gl1cuqbUotg" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/04/why-twitter-clients-dont-matter.html/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/04/why-twitter-clients-dont-matter.html</feedburner:origLink></item>
		<item>
		<title>Managing social data in Silverlight: do you push or pull?</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/4aRVh9c2eyA/managing-social-data-in-silverlight-do-you-push-or-pull.html</link>
		<comments>http://dimebrain.com/2009/01/managing-social-data-in-silverlight-do-you-push-or-pull.html#comments</comments>
		<pubDate>Wed, 28 Jan 2009 00:39:55 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[data]]></category>
		<category><![CDATA[pull]]></category>
		<category><![CDATA[push]]></category>
		<category><![CDATA[silverlight]]></category>

		<guid isPermaLink="false">http://dimebrain.com/?p=475</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fmanaging-social-data-in-silverlight-do-you-push-or-pull.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fmanaging-social-data-in-silverlight-do-you-push-or-pull.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Here&amp;#8217;s a problem you want to have&lt;/em&gt;: many concurrent connections to your web application, all vying for your precious data. Before you enjoy the challenges of that problem, you should consider whether your application works best in a push or pull scenario. In summary, pull applications are the most common; any time your client makes a service call to your application or another, expecting relevant data in the response, you are &amp;#8220;pulling&amp;#8221; data from a source and into your application. This is a fine way to go, as you are only keeping a thread in use long enough to&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fmanaging-social-data-in-silverlight-do-you-push-or-pull.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fmanaging-social-data-in-silverlight-do-you-push-or-pull.html" height="61" width="51" /></a></div><p><em>Here&#8217;s a problem you want to have</em>: many concurrent connections to your web application, all vying for your precious data. Before you enjoy the challenges of that problem, you should consider whether your application works best in a push or pull scenario. In summary, pull applications are the most common; any time your client makes a service call to your application or another, expecting relevant data in the response, you are &#8220;pulling&#8221; data from a source and into your application. This is a fine way to go, as you are only keeping a thread in use long enough to retrieve your data or an error, and you&#8217;re off to greater things.</p>
<p><strong>And then things go south</strong></p>
<p>But what if your application is social, and the underlying data changes frequently? Let&#8217;s use <a href="http://twitter.com/dimebrain" target="_blank">Twitter</a> as the typical example. Caching restrictions aside, how often do you want each and every one of your users to hit your server asking for the latest tweets? The numbers add up quickly if you have a popular application, and bandwidth is still a factor, even if you&#8217;re holding on to the actual data from Twitter for dear life.</p>
<p><strong>Pushing through the dip</strong></p>
<p>Have you heard of <a href="http://www.gnipcentral.com/" target="_blank">gnip</a>? It is a freemium service that solves this problem for popular applications: rather than having clients continously pull data from a server, gnip lets your applications know <em>when</em> there is fresh data available to be had on social sites, so you only use exactly the amount of bandwidth you need to update your data. Even with a server set up to intelligently update its back-end social stores, you may also want to consider having a similar set up for your Silverlight (and any WCF-supported client) applications: your clients are &#8220;pushed&#8221; data when its new and relevant, with no polling required. Of course there are performance considerations to consider in this scenario as well, since your server will need to track &#8220;who&#8221; is listening for messages, but this may work well for the application you&#8217;re building, as it&#8217;s working well for mine.</p>
<p><strong>Building a push client</strong></p>
<p>A push client is made possible through WCF&#8217;s duplex polling feature. Most of the plumbing for this example comes directly from MSDN&#8217;s article, so if you care deeply about the implementation details then I suggest you <a href="http://msdn.microsoft.com/en-us/library/cc645027(VS.95).aspx" target="_blank">start here first</a>. A duplex service defines two contracts, the one for messages your client sends to the server to let it know its alive, and the one your server &#8220;calls back&#8221; to the client; the latter contains all of the messages we can push to our Silverlight applications.</p>
<p>Let&#8217;s build a Twitter client that receives fresh tweet statuses only when they are available, rather than asking for new statuses from the server at regular intervals, like we might conceive of in a &#8220;pull&#8221;-based application. We&#8217;ll use <a href="http://code.google.com/p/tweetsharp">tweet#</a> to handle the communication between Twitter and the server.</p>
<pre name="code" class="csharp:nogutter">
// These are the messages we can send from
/// the client to the server
[ServiceContract(Namespace = "Push.Server",
                 CallbackContract = typeof(ITwitterClient))]
public interface ITwitterService
{
    [OperationContract(IsOneWay = true)]
    void SignIn(Message receivedMessage);

    [OperationContract(IsOneWay = true)]
    void SignOut(Message receivedMessage);
}

// These are our "push" messages from
// the server back to the client
[ServiceContract]
public interface ITwitterClient
{
    [OperationContract(IsOneWay = true)]
    void ReceivedStatuses(Message returnMessage);

    [OperationContract(IsOneWay = true)]
    void SignedIn(Message returnMessage);

    [OperationContract(IsOneWay = true)]
    void SignedOut(Message returnMessage);
}
</pre>
<p>Notice we&#8217;re defining the callback contract for the main service, and all messages are one way. We definitely don&#8217;t want to block any threads as messages are sent and received, and we need to distinguish between what service operation is doing the pushing (in this case, signing in, signing out, and receiving the latest tweets) so our client can respond appropriately.</p>
<p>When a user signs in, we want to store a reference to the connecting client, and push data updates to them when relevant. A Twitter application likely has a central store of &#8220;tweets&#8221;, that is the same for each client; when new data is available, every client that is signed in will receive the same information. In the implementation of our service we&#8217;ll spin off a new thread that polls Twitter in the background looking for a response that differs from our last poll. We&#8217;ll cache this as a static variable containing the most recent update.</p>
<pre name="code" class="csharp:nogutter">
static TwitterService()
{
    StartStatusesQuery();
}

private static void StartStatusesQuery()
{
    // Build a request to get the application's followers
    var request = FluentTwitter.CreateRequest()
        .Statuses().OnPublicTimeline().AsJson()
        .Configuration.CacheUntil(6.Seconds().FromNow())
        .CallbackTo(ProcessStatusesResponse);

    // Re-query every five seconds
    new Timer(callback =>
              {
                  _statusesQuery = request.Root;
                  _statusesQuery.RequestAsync();
              }, null, 0, 5000);
}
</pre>
<p>In the query callback, we&#8217;ll check to see if the JSON reply from Twitter is different than the previous result. If it is, we&#8217;ll call up all of the signed in clients and let them know.</p>
<pre name="code" class="csharp:nogutter">
private static void ProcessStatusesResponse
        (object sender, WebQueryResponseEventArgs e)
{
    var json = e.Response;
    if (json == null)
    {
        return;
    }

    try
    {
        var same = _statusesResult != null ?
                   _statusesResult.Equals(json) :
                   false;

        if (String.IsNullOrEmpty(_statusesResult) || !same)
        {
            _statusesResult = json;
            var message = string.Format("{0}#{1}",
                                        STATUSES_ACTION,
                                        _statusesResult);

            CallClients(message, STATUSES_ACTION, (c, m) => c.ReceivedStatuses(m));
        }
    }
    catch (TimeoutException)
    {
        //
    }
    catch (CommunicationException)
    {
        //
    }
}
</pre>
<p>Now on the client we can demonstrate how to manage an open channel back to the server and work with the messages that we receive. </p>
<pre name="code" class="csharp:nogutter">
public Page()
{
    InitializeComponent();
    _ui = SynchronizationContext.Current;

    // Create a new manager to receive data pushes from the server
    _manager = new ChannelManager(_ui);
    ChannelManager.ResponseReceived += manager_ResponseReceived;

    StartTweetTicker();
}

private void StartTweetTicker()
{
    var timer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 0, 2)};
    timer.Tick += ((sender, e) =>
                   {
                       if (_queue.Count == 0)
                       {
                           return;
                       }
                       var status = _queue.Dequeue();
                       _statuses.Add(status);
                       var tweet = new TextBlock
                               {
                                   Text = String.Format("{0}: {1}", status.User.ScreenName, status.Text),
                                   Width = 500,
                                   Height = 20
                               };

                       // This came from a different thread
                       _ui.Post(s => waterfall.Children.Add(tweet), null);
                       _ui.Post(s => UpdateQueue(), null);
                   });
    timer.Start();
}
</pre>
<p>In the page constructor above, we&#8217;re creating a new channel manager, a convenience class that wraps up the duplex polling logic, and passing it a handler for when a new server-side message is received. Then we make a call to spin up a new timer thread that &#8220;peels&#8221; from our batch of tweets and displays them on-screen.</p>
<pre name="code" class="csharp:nogutter">
void manager_ResponseReceived(object sender, DuplexResponseEventArgs e)
{
    switch (e.Action)
    {
        case ChannelManager.STATUSES_ACTION:
        {
            var statuses = e.Response.AsStatuses();
            var fresh = statuses.Where(s => !_queue.Contains(s) &#038;&#038; !_statuses.Contains(s));
            foreach (var status in fresh)
            {
                 _queue.Enqueue(status);
            }

            // This came from a different thread
            _ui.Post(s => UpdateQueue(), null);
            break;
        }
        case ChannelManager.SIGNED_IN_ACTION:
            break;
        case ChannelManager.SIGNED_OUT_ACTION:
            break;
        default:
            throw new NotSupportedException("Unknown callback contract operation");
    }
}
</pre>
<p>Above, we&#8217;re handling the response to each unique message action, whether it&#8217;s confirming sign-in and sign-out, or receiving new tweets. In the latter case, we make sure we aren&#8217;t looking at any duplicate tweets from those we&#8217;ve received before, before queueing them up for display.</p>
<p>I won&#8217;t dwell on the fine details of the channel management code itself; for that you can refer to the MSDN article mentioned earlier. I have prepared a reference application you can use to see the duplex behavior in action. I like how <a href="http://twitterfall.com">twitterfall.com</a> queues and then &#8220;peels&#8221; tweets off the queue for display; the reference application shows this type of behavior in action. </p>
<p><a href="http://dimebrain.com.s3.amazonaws.com/PushClient.zip">Download the example solution</a></p>
<p>It&#8217;s probably true that for many situations a pull-based architecture is appropriate. How well do you think push-based services scale? You can use this example as an exploration for your own applications.</p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=CMpkyNgJ"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=CMpkyNgJ" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=041H7Ee1"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=MbWaGWLh"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=MbWaGWLh" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=agNMvxvJ"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/4aRVh9c2eyA" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/01/managing-social-data-in-silverlight-do-you-push-or-pull.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/01/managing-social-data-in-silverlight-do-you-push-or-pull.html</feedburner:origLink></item>
		<item>
		<title>‘Innovate with Silverlight 2′ article code available</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/GYDtO_ixt4Y/innovate-with-silverlight-2-article-code-available.html</link>
		<comments>http://dimebrain.com/2009/01/innovate-with-silverlight-2-article-code-available.html#comments</comments>
		<pubDate>Sat, 24 Jan 2009 01:55:00 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Old]]></category>

		<guid isPermaLink="false">http://dimebrain.com/?p=458</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Finnovate-with-silverlight-2-article-code-available.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Finnovate-with-silverlight-2-article-code-available.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;Back in October, I wrote an article for MSDN&amp;#8217;s &lt;a href="http://www.microsoft.com/youshapeit/"&gt;{youshapeit}&lt;/a&gt; campaign called &amp;#8220;&lt;a href="http://www.microsoft.com/youshapeit/msdn/ExpertKnowledge/2008-11/InnovateWithSilverlight2.aspx" target="_blank"&gt;Innovate with Silverlight 2: Community-powered client side imaging&lt;/a&gt;&amp;#8220;, outlining several open source approaches for displaying and editing GIF, PNG, BMP, and JPEG images in Silverlight 2 exclusively from the client-side.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;ve received quite a few messages over the last while notifying me that the source code for the article is no longer available on the site. Luckily, I was given permission to share that code, so you may &lt;a href="http://dimebrain.com.s3.amazonaws.com/PhotoCropper.zip"&gt;download it here&lt;/a&gt;. Please refer to the article for a description of licensing terms.&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Finnovate-with-silverlight-2-article-code-available.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Finnovate-with-silverlight-2-article-code-available.html" height="61" width="51" /></a></div><p>Back in October, I wrote an article for MSDN&#8217;s <a href="http://www.microsoft.com/youshapeit/">{youshapeit}</a> campaign called &#8220;<a href="http://www.microsoft.com/youshapeit/msdn/ExpertKnowledge/2008-11/InnovateWithSilverlight2.aspx" target="_blank">Innovate with Silverlight 2: Community-powered client side imaging</a>&#8220;, outlining several open source approaches for displaying and editing GIF, PNG, BMP, and JPEG images in Silverlight 2 exclusively from the client-side.</p>
<p>I&#8217;ve received quite a few messages over the last while notifying me that the source code for the article is no longer available on the site. Luckily, I was given permission to share that code, so you may <a href="http://dimebrain.com.s3.amazonaws.com/PhotoCropper.zip">download it here</a>. Please refer to the article for a description of licensing terms.</p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=U2waQ3Zs"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=U2waQ3Zs" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=I2Ag26OJ"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=wPLs8jeI"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=wPLs8jeI" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=JYgfby7G"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/GYDtO_ixt4Y" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/01/innovate-with-silverlight-2-article-code-available.html/feed</wfw:commentRss>
		<slash:comments>6</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/01/innovate-with-silverlight-2-article-code-available.html</feedburner:origLink></item>
		<item>
		<title>People + Media + Messages = Social</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/eSY28P19CZU/people-media-messages-social.html</link>
		<comments>http://dimebrain.com/2009/01/people-media-messages-social.html#comments</comments>
		<pubDate>Mon, 19 Jan 2009 11:32:57 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Conversations]]></category>
		<category><![CDATA[media]]></category>
		<category><![CDATA[messages]]></category>
		<category><![CDATA[people]]></category>
		<category><![CDATA[social]]></category>

		<guid isPermaLink="false">http://s57219.gridserver.com/?p=148</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fpeople-media-messages-social.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fpeople-media-messages-social.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;In a social application there are always two domains: the domain you define, or the ‘problem’ domain, and the social domain. A video-sharing web application, for instance, has no problem domain. Its entire story can be described within the social domain: it has users, videos, possibly forums, possibly blogs, possibly syndication of content. In other words, there are patterns we can apply that allow us to describe this system to a framework without writing any code, at least not code that isn&amp;#8217;t contained within some higher API-level abstraction. If we aren&amp;#8217;t there today, then we should move in that&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fpeople-media-messages-social.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fpeople-media-messages-social.html" height="61" width="51" /></a></div><p>In a social application there are always two domains: the domain you define, or the ‘problem’ domain, and the social domain. A video-sharing web application, for instance, has no problem domain. Its entire story can be described within the social domain: it has users, videos, possibly forums, possibly blogs, possibly syndication of content. In other words, there are patterns we can apply that allow us to describe this system to a framework without writing any code, at least not code that isn&#8217;t contained within some higher API-level abstraction. If we aren&#8217;t there today, then we should move in that direction.</p>
<p>Whatever is unique about this video-sharing application compared to all the others, then, is the element we can design and layer on top of the framework. That is the element that we find interesting, that teammates are passionate about, and that the Internet population will find exciting (we hope). The more we are able to focus on this element, the more enjoyable the process will be. It is also a matter of necessity: any application that does nothing more than re-iterate the acceptable interactions in a social domain adds nothing to the conversation, and is ultimately forgettable. Thinking that you can &#8220;just add blogs and tags&#8221; is not going to take you very far.</p>
<p>The crux of a social application is interaction. If social software had a paradigm to follow it might be called Interaction Driven Design. In &#8220;<a href="http://www.amazon.com/Designing-Social-Voices-That-Matter/dp/0321534921/ref=dimebrain-20?ie=UTF8&amp;s=books&amp;qid=1232367957&amp;sr=8-1">Designing for the Social Web</a>&#8220;, Joshua Porter calls this the AOF pattern, or Activities, Objects, and Features. We&#8217;re designing for the need for your application to arrange itself so that it is evident what a user does, and what benefit they get from doing so.</p>
<p>The application you build is merely the implementation details of taking a description of a problem domain, and how it is allowed to interact with itself and other domains, and coupling it with a description of how its users may interact with each other. The more we can describe about those interactions up front, the less code we need to write. It is fortunate, then, that we already know much about the social domain, and it enjoys a healthy amount of overlap between the typical objects that we might find in a social application: people + messages + media. Are there any social objects that don&#8217;t fit one of these categories, and are the contracts between these categories not well-defined?</p>
<p>This blog was formed to explore and define the social domain, that which you can separate from your &#8220;secret sauce&#8221; and is the most likely candidate for abstraction. The principle we want to follow is that choosing to build social software in .NET should mean that we get to move from a great software idea to an implementation that already possesses the social domain, so that the harder work of creating your own interactions, the ones that make your application unique, is both more apparent and has common ground to relate with.</p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=7Z29DoKA"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=7Z29DoKA" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=QMKwu1Kn"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=siMsDsSK"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=siMsDsSK" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=pMlb8B5J"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/eSY28P19CZU" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/01/people-media-messages-social.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/01/people-media-messages-social.html</feedburner:origLink></item>
		<item>
		<title>Introducing tweet#, the complete fluent C# library for Twitter</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/GDB3B7mjN9g/introducing-tweet-the-complete-fluent-c-library-for-twitter.html</link>
		<comments>http://dimebrain.com/2009/01/introducing-tweet-the-complete-fluent-c-library-for-twitter.html#comments</comments>
		<pubDate>Sat, 10 Jan 2009 09:21:00 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[api]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[tweetsharp]]></category>
		<category><![CDATA[twitter]]></category>

		<guid isPermaLink="false">http://dimebrain.com/?p=400</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fintroducing-tweet-the-complete-fluent-c-library-for-twitter.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fintroducing-tweet-the-complete-fluent-c-library-for-twitter.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;tweet# is a complete C# library for Twitter that allows you to write short and sweet expressions that are automatically converted to Twitter queries and sent on your behalf. In addition to covering 100% of Twitter&amp;#8217;s REST and Search APIs, tweet# also provides the following configurable features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Discoverable method chains that only expose what you can actually call&lt;/li&gt;
&lt;li&gt;Hooks for easy asynchronous operation&lt;/li&gt;
&lt;li&gt;Status update truncation by word, and automated URL shortening&lt;/li&gt;
&lt;li&gt;Server-side caching of queries (ASP.NET, memcached, or use your own)&lt;/li&gt;
&lt;li&gt;Quick and painless conversion from JSON query results to C# data classes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are just a few examples to show you what&amp;#8217;s possible&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fintroducing-tweet-the-complete-fluent-c-library-for-twitter.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2009%2F01%2Fintroducing-tweet-the-complete-fluent-c-library-for-twitter.html" height="61" width="51" /></a></div><p>tweet# is a complete C# library for Twitter that allows you to write short and sweet expressions that are automatically converted to Twitter queries and sent on your behalf. In addition to covering 100% of Twitter&#8217;s REST and Search APIs, tweet# also provides the following configurable features:</p>
<ul>
<li>Discoverable method chains that only expose what you can actually call</li>
<li>Hooks for easy asynchronous operation</li>
<li>Status update truncation by word, and automated URL shortening</li>
<li>Server-side caching of queries (ASP.NET, memcached, or use your own)</li>
<li>Quick and painless conversion from JSON query results to C# data classes</li>
</ul>
<p>Here are just a few examples to show you what&#8217;s possible with tweet#:</p>
<pre name="code" class="csharp:nogutter">
// Get the public timeline, caching the result for two minutes
var twitter = FluentTwitter.CreateRequest()
     .Configuration.CacheUntil(2.Minutes().FromNow())
     .Statuses().OnPublicTimeline().AsJson();

// Send a blocking call
var response = twitter.Request();

// Convert response to data classes
var statuses = response.AsStatuses();
</pre>
<p>With plans to support <a href="http://oauth.net" target="_blank">OAuth</a>, selectors, rate limiting, and geocoding enhancements, tweet# should accelerate your Twitter client development.</p>
<pre name="code" class="csharp:nogutter">
// Send a status update with automatic URL shortening
var twitter = FluentTwitter.CreateRequest()
     .Configuration.UseUrlShortening()
     .AuthenticateAs(username, password)
     .Statuses().Update("http://code.google.com/p/tweetsharp")
     .CallbackTo((s, e) =>
                     {
                          // do stuff asynchronously...
                          var updated = e.Response.AsStatus();
                     }.AsXml();

// Send an asynchronous call
twitter.RequestAsync();
</pre>
<p>Feedback on design, features, and issues is greatly appreciated. The best way to get acquainted with tweet# syntax and style, currently, is to download the code or browse the unit tests.</p>
<p>I hope you find it useful!</p>
<p>Project code is hosted by Google Code:<br />
<a title="tweetsharp" href="http://code.google.com/p/tweetsharp" target="_blank">http://code.google.com/p/tweetsharp</a></p>
<p><em>tweet# uses James Newton-King&#8217;s <a title="JSON.NET" href="http://james.newtonking.com/pages/json-net.aspx" target="_blank">JSON.NET</a>, the <a href="http://www.codeplex.com/EnyimMemcached">Enyim Memcached Client</a>, and would like to thank Sean Erickson for his assistance with multi-part form posts.</em></p>
<p><a href="http://www.dotnetkicks.com/kick/?url=http%3a%2f%2fdimebrain.com%2f2009%2f01%2fintroducing-tweet-the-complete-fluent-c-library-for-twitter.html"><img src="http://www.dotnetkicks.com/Services/Images/KickItImageGenerator.ashx?url=http%3a%2f%2fdimebrain.com%2f2009%2f01%2fintroducing-tweet-the-complete-fluent-c-library-for-twitter.html&#038;bgcolor=FF9933&#038;cbgcolor=FFFFCC" border="0" alt="kick it on DotNetKicks.com" /></a></p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=lkkDQVlN"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=lkkDQVlN" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=5Qi8FZWR"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=jxQnaYNe"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=jxQnaYNe" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=MRamTg4P"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/GDB3B7mjN9g" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2009/01/introducing-tweet-the-complete-fluent-c-library-for-twitter.html/feed</wfw:commentRss>
		<slash:comments>7</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2009/01/introducing-tweet-the-complete-fluent-c-library-for-twitter.html</feedburner:origLink></item>
		<item>
		<title>How to make cross-site service calls in Silverlight using JSON</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/eP2MDMY8BUQ/how-to-make-cross-site-service-calls-in-silverlight-using-json.html</link>
		<comments>http://dimebrain.com/2008/12/how-to-make-cross-site-service-calls-in-silverlight-using-json.html#comments</comments>
		<pubDate>Mon, 22 Dec 2008 13:31:01 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Old]]></category>

		<guid isPermaLink="false">http://s57219.gridserver.com/2008/12/how-to-make-cross-site-service-calls-in-silverlight-using-json.html</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fhow-to-make-cross-site-service-calls-in-silverlight-using-json.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fhow-to-make-cross-site-service-calls-in-silverlight-using-json.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;You’re quite familiar by now with the need for cross-domain policy files and service proxies if you’ve spent any time writing applications in Silverlight that behave like mashups. The root of the cross-domain issue is in how browsers restrict XML requests, and one emerging and popular way to circumvent this problem is to make JSON calls instead, which are cross-site friendly.&lt;/p&gt;
&lt;p&gt;Silverlight has a nice set of browser interoperability classes waiting to be used for these kinds of purposes. As a quick and dirty example let’s create some extensions that allow us to make JSON calls to grab data from&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fhow-to-make-cross-site-service-calls-in-silverlight-using-json.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fhow-to-make-cross-site-service-calls-in-silverlight-using-json.html" height="61" width="51" /></a></div><p>You’re quite familiar by now with the need for cross-domain policy files and service proxies if you’ve spent any time writing applications in Silverlight that behave like mashups. The root of the cross-domain issue is in how browsers restrict XML requests, and one emerging and popular way to circumvent this problem is to make JSON calls instead, which are cross-site friendly.</p>
<p>Silverlight has a nice set of browser interoperability classes waiting to be used for these kinds of purposes. As a quick and dirty example let’s create some extensions that allow us to make JSON calls to grab data from <a href="http://twitter.com">Twitter</a>.</p>
<p>The first thing we’ll do is create an event class that manages incoming JSON data received from a service call. We’ll want to work with objects rather than a string, so I’ve <a href="http://stackoverflow.com/questions/199718/can-you-instantiate-an-object-instance-from-json-in-net">borrowed some code</a> from a conversation on <a href="http://www.stackoverflow.com">StackOverflow</a> to provide a simple conversion from a JSON string to a dictionary of property names matched to objects. You can use whatever JSON converter you like, or <a href="http://www.box.net/shared/static/x5z0icithg.cs">download my version</a>.</p>
<p><strong>JsonEvent.cs</strong></p>
<pre name="code" class="csharp:nogutter">
[ScriptableType]
public class JsonEvent
{
    public static event EventHandler&lt;JsonEventArgs> Responded;
    public static void OnResponded(string json)
    {
        if(Responded != null)
        {
            Responded(null, new JsonEventArgs(json.ToDictionary()));
        }
    }

    [ScriptableMember]
    public void Received(string json)
    {
        OnResponded(json);
    }
}
</pre>
<p><strong>JsonEventArgs.cs</strong></p>
<pre name="code" class="csharp:nogutter">
public class JsonEventArgs : EventArgs
{
    public IDictionary&lt;string, object> Data
    {
        get;
        private set;
    }   

    public JsonEventArgs(IDictionary&lt;string, object> data)
    {
        Data = data;
    }
}
</pre>
<p>Now let’s take a look at the extension method that’s going to perform the service call for us.</p>
<p><strong>JsonExtensions.cs</strong></p>
<pre name="code" class="csharp:nogutter">
public static class JsonExtensions
{
    private static readonly Dictionary&lt;string, object>
        _scriptables = new Dictionary&lt;string, object>();     

    public static void SendJson(this string url)
    {
        if(!_scriptables.ContainsKey("Json"))
        {
            HtmlPage.RegisterScriptableObject("Json", new JsonEvent());
        }

        var id = HtmlPage.Plugin.Id;
        HtmlPage.Window.Invoke("jsonLoad", url, id);
    }
}
</pre>
<p>In the snippet above, we’ve registered a new instance of our JsonEvent class to receive incoming data, retrieved the name of the Sys.UI.Silverlight.Control instance on our host page, and called a JavaScript method called jsonLoad(), passing it the web request URL, and the control’s ID.</p>
<p>On the client-side, we’ll need the usual script to handle JSON data (<a href="http://www.json.org/json2.js">www.json.org/json2.js</a> for example), as well as a few lines to make the JSON call, and add a callback to our scriptable method registered on JsonEvent.</p>
<p><strong>SilverlightJson.js</strong></p>
<pre name="code" class="csharp:nogutter">
var $id;

function jsonLoad(url, id)
{
    $id = id;
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;
    script.src += hasParameters(url) ? '&#038;' : '?';
    script.src += 'callback=jsonCallback';

    var head = document.getElementsByTagName('head')[0];
    head.appendChild(script);
};

function jsonCallback(jsonData)
{
    var id = $id;
    var silverlight = document.getElementById(id);

    if (silverlight)
    {
        var response = JSON.stringify(jsonData);
        silverlight.Content.Json.Received(response);
    }
};

function hasParameters(url)
{
    url = url.replace(/[[]/, "\[").replace(/[]]/, "\]");
    var pattern = "[\?]w+=([^&#]*)";
    var regex = new RegExp(pattern);
    var results = regex.exec(url);

    return results != null;
}
</pre>
<p>With the script in place, making a client-side call to Twitter is easy.</p>
<pre name="code" class="csharp:nogutter">
public partial class Page
{
    public Page()
    {
        InitializeComponent();
        JsonEvent.Responded += JsonEvent_Responded;

        "http://twitter.com/users/show/dimebrain.json".SendJson();
    }

    static void JsonEvent_Responded(object sender, JsonEventArgs e)
    {
        // Twitter response is in key-value pairs
        var name = e.Data["name"];
    }
}
</pre>
<p>This approach requires that the service API you’re calling supports the &#8220;callback” parameter, but the popularity and simplicity of using JSON on the client should see many of the most used APIs, like Twitter, providing this feature. Whether you stick with proxies or use this method, hopefully you have a better understanding of what is possible through the browser and Silverlight in tandem.</p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=FT3M0uuT"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=FT3M0uuT" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=vvUT1j5I"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=kK67PVry"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=kK67PVry" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=046SyQYO"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/eP2MDMY8BUQ" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2008/12/how-to-make-cross-site-service-calls-in-silverlight-using-json.html/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2008/12/how-to-make-cross-site-service-calls-in-silverlight-using-json.html</feedburner:origLink></item>
		<item>
		<title>Composing animations with Silverlight.FX</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/mxV0NfeJj4o/composing-animations-with-silverlightfx.html</link>
		<comments>http://dimebrain.com/2008/12/composing-animations-with-silverlightfx.html#comments</comments>
		<pubDate>Fri, 19 Dec 2008 15:00:00 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Old]]></category>

		<guid isPermaLink="false">http://s57219.gridserver.com/2008/12/composing-animations-with-silverlightfx.html</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fcomposing-animations-with-silverlightfx.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fcomposing-animations-with-silverlightfx.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;When I read Nikhil’s “&lt;a href="http://www.nikhilk.net/Silverlight-Effects-Transitions.aspx"&gt;Effects and Transitions for Silverlight&lt;/a&gt;” and its accompanying &lt;a href="http://github.com/NikhilK/silverlightfx/tree/master"&gt;Silverlight.FX&lt;/a&gt; framework, I was impressed by how easy it was, and is, to create an animation and declare it on any Silverlight framework element.&lt;/p&gt;
&lt;pre name="code" class="html:nogutter"&gt;&amp;#60;Grid&amp;#62;
    &amp;#60;Grid.RowDefinitions&amp;#62;...&amp;#60;/Grid.RowDefinitions&amp;#62;
        &amp;#60;fxglitz:Effects.ClickEffect&amp;#62;
            &amp;#60;fxglitz:HighlightEffect TargetName="highlightImage" HighlightColor="Yellow" Duration="00:00:01" /&amp;#62;
        &amp;#60;/fxglitz:Effects.ClickEffect&amp;#62;
        &amp;#60;Border x:Name="highlightImage"&amp;#62;
            &amp;#60;Image Source="/Silverlight.png" /&amp;#62;
        &amp;#60;/Border&amp;#62;
        &amp;#60;TextBlock Grid.Row="1"&amp;#62;Highlight&amp;#60;/TextBlock&amp;#62;
&amp;#60;/Grid&amp;#62;&lt;/pre&gt;
&lt;p&gt;With a framework this powerful, I wanted to extend it even further by providing a fluent interface, similar to &lt;a href="http://jquery.com"&gt;jQuery&lt;/a&gt;, to compose and chain animations together. This way, you can declare a common animation element and behavior once, and apply it to any framework element you require, swap&amp;#8230;&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fcomposing-animations-with-silverlightfx.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fcomposing-animations-with-silverlightfx.html" height="61" width="51" /></a></div><p>When I read Nikhil’s “<a href="http://www.nikhilk.net/Silverlight-Effects-Transitions.aspx">Effects and Transitions for Silverlight</a>” and its accompanying <a href="http://github.com/NikhilK/silverlightfx/tree/master">Silverlight.FX</a> framework, I was impressed by how easy it was, and is, to create an animation and declare it on any Silverlight framework element.</p>
<pre name="code" class="html:nogutter">&lt;Grid&gt;
    &lt;Grid.RowDefinitions&gt;...&lt;/Grid.RowDefinitions&gt;
        &lt;fxglitz:Effects.ClickEffect&gt;
            &lt;fxglitz:HighlightEffect TargetName="highlightImage" HighlightColor="Yellow" Duration="00:00:01" /&gt;
        &lt;/fxglitz:Effects.ClickEffect&gt;
        &lt;Border x:Name="highlightImage"&gt;
            &lt;Image Source="/Silverlight.png" /&gt;
        &lt;/Border&gt;
        &lt;TextBlock Grid.Row="1"&gt;Highlight&lt;/TextBlock&gt;
&lt;/Grid&gt;</pre>
<p>With a framework this powerful, I wanted to extend it even further by providing a fluent interface, similar to <a href="http://jquery.com">jQuery</a>, to compose and chain animations together. This way, you can declare a common animation element and behavior once, and apply it to any framework element you require, swap it for another, or chain reusable components together.</p>
<p>Here is the equivalent code for the highlight example above, using the new fluent interface:</p>
<pre name="code" class="csharp:nogutter">Animator.WithNew(highlightImage)
        .Highlight(Colors.Yellow)
        .For(1.Seconds())
        .When(EffectBehaviors.Clicked)
        .Play();</pre>
<p>So far we have a nice, code-centric way to declare these powerful effects (or any custom effects you create in addition to the ones provided with FX). Now we can take it even further by providing animation chaining to achieve the results of the CompositeAnimation effect in a natural way.</p>
<pre name="code" class="csharp:nogutter">&lt;fxui:ClickEffect&gt;
    &lt;fxeffects:CompositeEffect AutoReverse="True" Composition="Parallel"&gt;
        &lt;fxeffects:Move TargetName="compositeImage" HorizontalMovement="50" Duration="00:00:01" Easing="QuadraticInOut" /&gt;
        &lt;fxeffects:Resize TargetName="compositeImage" ScaleXRatio="0.5" ScaleYRatio="0.5" Duration="00:00:01" Easing="QuadraticInOut" /&gt;
        &lt;fxeffects:Spin TargetName="compositeImage" SpinAngle="360" Duration="00:00:01" Easing="QuadraticInOut" /&gt;
        &lt;fxeffects:Fade TargetName="compositeImage" FadeOpacity="0.5" Duration="00:00:01" Easing="QuadraticInOut" /&gt;
    &lt;/fxeffects:CompositeEffect&gt;
&lt;/fxui:ClickEffect&gt;</pre>
<p>The above is nice and declarative, but you can achieve better readability and flow in the code below. In addition to parallel animations, you can also mix it up with sequential animations using the <span style="font-family: consolas">Then()</span> method.</p>
<pre name="code" class="csharp:nogutter">Animator.WithNew(compositeImage)
     .WithEasing(EffectEasing.QuadraticInOut)
     .Move(50).For(1.Seconds())
     .And().Resize(0.5, 0.5).For(1.Seconds())
     .And().Spin(360).For(1.Seconds())
     .And().FadeTo(0.5).For(1.Seconds())
     .Play();</pre>
<p>You can also predefine animations and remix them at runtime using the the new interface. For example, the composite effect above is made up of four separate animations. Using the declarative approach we would have to create a new effect that wraps all of those effects, or rewrite them every time we wanted that particular combination of effects. Using a fluent interface we can declare each animation separately, and use them in one or many places interchangeably.</p>
<pre name="code" class="csharp:nogutter">// Move script (used on another image)
var move = Animator.WithNew().Move(50).For(1.Seconds());
move.On(moveImage).Play();

// Resize script (used on another image)
var resize = Animator.WithNew().Resize(0.5, 0.5).For(1.Seconds());
resize.On(resizeImage).Play();

// Spin script (used on another image)
var spin = Animator.WithNew().Spin(360).For(1.Seconds());
spin.On(spinImage).Play();

// An animation on a new target but reusing
// the animations used on other targets
Animator.WithNew(compositeImage)
    .WithEasing(EffectEasing.QuadraticInOut)
    .FadeTo(0.5).For(1.Seconds())
    .And(move)
    .And(resize)
    .And(spin)
    .When(EffectBehaviors.Clicked)
    .Play();</pre>
<p>The results above are achieved by combining animators’ contents together in the <span style="font-family: consolas">And()</span> overloaded method. You are free to download the project, which includes the same effects examples from Silverlight.FX using the new fluent interface in the code-behind.</p>
<p>Enjoy!<br />
Downloads: <a href="http://dimebrain.com.s3.amazonaws.com/AnimationComposer.zip">AnimationComposer</a></p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=GD5JKw18"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=GD5JKw18" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=M8NGRRVd"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=mNs19w7U"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=mNs19w7U" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=82MDrDtQ"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/mxV0NfeJj4o" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2008/12/composing-animations-with-silverlightfx.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2008/12/composing-animations-with-silverlightfx.html</feedburner:origLink></item>
		<item>
		<title>Jumping from ASP.NET to Silverlight 2 available now</title>
		<link>http://feedproxy.google.com/~r/Dimebrain/~3/2CdntpZonZU/jumping-from-aspnet-to-silverlight-2-available-now.html</link>
		<comments>http://dimebrain.com/2008/12/jumping-from-aspnet-to-silverlight-2-available-now.html#comments</comments>
		<pubDate>Tue, 16 Dec 2008 18:40:46 +0000</pubDate>
		<dc:creator>Daniel</dc:creator>
				<category><![CDATA[Old]]></category>

		<guid isPermaLink="false">http://s57219.gridserver.com/2008/12/jumping-from-aspnet-to-silverlight-2-available-now.html</guid>
		<description>&lt;div class="tweetmeme_button" style="float: right; margin-left: 10px;"&gt;&lt;a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fjumping-from-aspnet-to-silverlight-2-available-now.html"&gt;&lt;img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fjumping-from-aspnet-to-silverlight-2-available-now.html" height="61" width="51" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;img class="alignright size-full wp-image-59" title="Jumping from ASP.NET to Silverlight 2" src="http://dimebrain.com/wp-content/uploads/jumping-from-aspnet-to-silverlight2.jpg" alt="Jumping from ASP.NET to Silverlight 2" width="100" height="129" /&gt;If you&amp;#8217;re not familiar with &lt;a href="http://www.wrox.com/WileyCDA/Section/Wrox-Blox-NEW.id-320868.html"&gt;Wrox Blox&lt;/a&gt;, they are shorter than books, and aim to give developers focused, contextual information on new technologies to get and stay productive quickly. I&amp;#8217;m happy to report that &lt;a href="http://www.wrox.com/WileyCDA/WroxTitle/Jumping-from-ASP-NET-to-Silverlight-2.productCd-0470477164.html"&gt;Jumping from ASP.NET to Silverlight 2&lt;/a&gt; was published today.&lt;/p&gt;
&lt;p&gt;In it, I try to provide a solid base from which to learn Silverlight 2 coming from an ASP.NET development background. If you&amp;#8217;ve held off getting into Silverlight 2, but are anxious to learn and build on what you already know about web development, I sincerely hope this is helpful for you.&lt;/p&gt;</description>
			<content:encoded><![CDATA[<div class="tweetmeme_button" style="float: right; margin-left: 10px;"><a href="http://api.tweetmeme.com/share?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fjumping-from-aspnet-to-silverlight-2-available-now.html"><img src="http://api.tweetmeme.com/imagebutton.gif?url=http%3A%2F%2Fdimebrain.com%2F2008%2F12%2Fjumping-from-aspnet-to-silverlight-2-available-now.html" height="61" width="51" /></a></div><p><img class="alignright size-full wp-image-59" title="Jumping from ASP.NET to Silverlight 2" src="http://dimebrain.com/wp-content/uploads/jumping-from-aspnet-to-silverlight2.jpg" alt="Jumping from ASP.NET to Silverlight 2" width="100" height="129" />If you&#8217;re not familiar with <a href="http://www.wrox.com/WileyCDA/Section/Wrox-Blox-NEW.id-320868.html">Wrox Blox</a>, they are shorter than books, and aim to give developers focused, contextual information on new technologies to get and stay productive quickly. I&#8217;m happy to report that <a href="http://www.wrox.com/WileyCDA/WroxTitle/Jumping-from-ASP-NET-to-Silverlight-2.productCd-0470477164.html">Jumping from ASP.NET to Silverlight 2</a> was published today.</p>
<p>In it, I try to provide a solid base from which to learn Silverlight 2 coming from an ASP.NET development background. If you&#8217;ve held off getting into Silverlight 2, but are anxious to learn and build on what you already know about web development, I sincerely hope this is helpful for you.</p>
<div class="feedflare">
<a href="http://feeds.feedburner.com/~f/Dimebrain?a=tCXzpNct"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=tCXzpNct" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=7fXYLztO"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=41" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=6WIPMgbq"><img src="http://feeds.feedburner.com/~f/Dimebrain?i=6WIPMgbq" border="0"></img></a> <a href="http://feeds.feedburner.com/~f/Dimebrain?a=QMOGgIgB"><img src="http://feeds.feedburner.com/~f/Dimebrain?d=52" border="0"></img></a>
</div><img src="http://feeds.feedburner.com/~r/Dimebrain/~4/2CdntpZonZU" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://dimebrain.com/2008/12/jumping-from-aspnet-to-silverlight-2-available-now.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		<feedburner:origLink>http://dimebrain.com/2008/12/jumping-from-aspnet-to-silverlight-2-available-now.html</feedburner:origLink></item>
	</channel>
</rss>
