Monday, September 21, 2009

Twitter And The Courtesy Of Retweeting

Twitter’s strong word-of-mouth characteristics are derived from the common courtesy of retweets of its user base, not on technology – this is changing with an announcement this week. This arbitrary courtesy is the flaw that could lead to its demise once Twitter becomes more commonplace and the tight knit group of early adopters finds that courtesy gets undermined. For example: I have 100 followers and I post an event notification for the small city I live in. One of my followers (call him @WhatcomTravel) decides that this posting is relevant to his/her followers and decides to retweet my posting. By common courteous he starts the post RT: @myusername which gives me credit for the original posting. If he has 2000 followers, some of them will start to follow me to if they feel my postings are relevant to their interest. In essence they go to the source. Here lies the issue; if they follow me it dilutes the attention that they pay to the person that retweeted my post (@WhatcomTravel). The more people they follow the more information they have to process. So, from a tactical perspective @WhatcomTravel shouldn’t post my twitter handle in the RT. If @WhatcomTravel wants to maintain a power position in the community as the end source for information, retweeting without reference makes him appear like the source of the information. Though I can block followers that in essence steal my tweets, it wouldn’t be advantageous if I was the promoter of the event and wanted to get the word out. Also, I would have to be diligent in searching for tweets that I post being re-tweeted without reference and then police those followers by blocking them. Any type of policing and blocking undermines the retweet magic of Twitter’s word of mouth communication. However, Twitter announced this week that it would embed retweeting into its API, allowing re-tweets to be handled by technology and not common courtesy. For the reasons stated above, I think this is a great idea. Reference: Twitter’s Upcoming Retweet Feature {6230289B-5BEE-409e-932A-2F01FA407A92}

Thursday, September 3, 2009

Not That Obvious In MVC

I am working on converting some ASP.NET WebForms pages into MVC Views and there are a couple of things I have run across in MasterPages that are not obvious in the beginning. With ASP.NET WebForms if you wanted dynamic content in your MasterPage one way to do it is to have the code behind of the MasterPage insert it, which means that your Page either need to tell the Master Page what to insert or the Master Page need to figure it out. Take this for example:
<head>
    <meta name="Description" content="" id="metaDescription" runat="server" />
</head>
Here I am trying to add a meta description the head of the HTML so that the search engines know what this page is about. What I was doing is have the page check the type of the master class on the pages OnInit event and if it was the right master class, find the public property in the MasterPage that would set the MetaDescription. When the MasterPage pre-rendered it would insert the value of that property into the content attribute of the server side control. With ASP.NET MVC you don't have code behind. It was not obvious to me (however it makes sense now) that the MasterPage has access to all the ViewData, so in MVC the above looks like this:
<head>
    <meta name="Description" content="<%=ViewData["MetaDescription"]%>"/>
</head>
All I have to do in this case is set the MetaDescription of the ViewData in the controller and it will get filled in on this MasterPage. It is not obvious also that if I don't set the ViewData for the MetaDescription then the content attribute gets an String.Empty. You might say: "That is how it should work?" Well it should, unless you consider that ViewData holds objects and has an arbitrary number of entries. So referencing an unset entry should give a KeyNotFoundException or maybe a NullReferenceException, however this has been cleanly handled in ASP.NET MVC. Which means that as I program the View, I don't need to worry about all the controllers that use this view setting the MetaDescription. If they set it great I will use it, otherwise it is blank. {6230289B-5BEE-409e-932A-2F01FA407A92}

Wednesday, September 2, 2009

Upgrading WebForms .csproj to MVC

So you followed all the steps to upgrade your old ASP.NET WebForms project to ASP.NET MVC: added the necessary entries to the web.config, created a Views and Controllers directory, and added the necessary references. However, when you right click to add a new item to the Controllers directory (or Views directory) in Visual Studio 2008 there are no MVC items available. You need to update the .csproj view a text editor to "tell" it that it is a MVC project. Here is how: 1. Close out of Visual Studio. 2. Open .csproj file of the web site in notepad 3. In the beginning of file there is PropertyGroup block. Find the ProjectTypeGuids tag. 4. Add {603c0e0b-db56-11dc-be95-000d561079b0} as first project type GUID, the delimiter is a semi-colon. 5. Save the file . Now you can open the solution again in Visual Studio 2008 and you will have the MVC items. {6230289B-5BEE-409e-932A-2F01FA407A92}

Update To RegExRoute Class

Update with a few new constructors that allow you to use the MvcRouteHandler, and some bug fixes.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Routing;
using System.Web.Mvc;
using System.Text.RegularExpressions;

namespace WebUtility
{
    public class RegexRoute : RouteBase
    {
        public Regex Regex { get; private set; }
        public String[] Groups { get; private set; }
        private IRouteHandler RouteHandler { get; set; }
        public String Controller { get; private set; }
        public String Action { get; private set; }

        /// <summary>
        /// Creates A Regular Expression Route
        /// </summary>
        /// <param name="regex">Regular Expression To Use Against
        /// the AbsolutePath of the request.</param>
        /// <param name="groups">roups In The Match</param>
        /// <param name="routeHandler">The name of the Handler 
        /// to use for this route.</param>
        public RegexRoute(Regex regex,
            String[] groups,
            IRouteHandler routeHandler)
        {
            Regex = regex;
            Groups = groups;
            RouteHandler = routeHandler;
        }

        /// <summary>
        /// Constructor that allows you to specify the
        /// controller and the action.
        /// </summary>
        /// <param name="regex">Regular Expression</param>
        /// <param name="groups">Groups In The Match</param>
        /// <param name="controller">Explicit Name Of The Controller</param>
        /// <param name="action">Explict Name Of The Action</param>
        public RegexRoute(Regex regex,
             String[] groups,
             String controller,
             String action)
        {
            Regex = regex;
            Groups = groups;
            Controller = controller;
            Action = action;
        }

        /// <summary>
        /// Constructor that assume controller and
        /// action are in the groups.
        /// </summary>
        /// <param name="regex"></param>
        /// <param name="groups"></param>
        public RegexRoute(Regex regex,
             String[] groups)
        {
            Regex = regex;
            Groups = groups;

            List<String> list = new List<String>(groups);

            if (!list.Contains("controller"))
                throw (new Exception(
                    "Controller group expected in regular expression match"));

            if (!list.Contains("action"))
                throw (new Exception(
                    "Action group expected in regular expression match"));
        }

        public override RouteData GetRouteData(
            System.Web.HttpContextBase httpContext)
        {
            MatchCollection matchCollection =
                Regex.Matches(httpContext.Request.Url.AbsolutePath);

            switch (matchCollection.Count)
            {
                case 0:
                    // WWB: There Is No Match --
                    //  This Route Doesn't Handle This URI
                    return (null);
                case 1:

                    // WWB: FillOut The Route Data
                    RouteData routeData = new RouteData();
                    routeData.Route = this;

                    if (RouteHandler != null)
                        routeData.RouteHandler = RouteHandler;
                    else
                        routeData.RouteHandler = new MvcRouteHandler();

                    if (!String.IsNullOrEmpty(Controller))
                        routeData.Values.Add("controller", Controller);

                    if (!String.IsNullOrEmpty(Action))
                        routeData.Values.Add("action", Action);

                    // WWB: No Group Names, No Values Outputted.
                    if (Groups != null)
                    {
                        // MSDN:The GroupCollection object returned
                        //      by the Match.Groups property
                        //      always has at least one member.

                        if (matchCollection[0].Groups.Count != Groups.Length)
                            throw (new Exception(String.Format(
                                "{0} contains {1} groups when matching {2}, however " +
                                "there are only {3} mappings.  There needs to be an " +
                                "equal number of mappings to groups, note that " +
                                "there is always one group the whole string.",
                                httpContext.Request.Url.AbsoluteUri,
                                matchCollection[0].Groups.Count,
                                Regex.ToString(),
                                Groups.Length)));

                        // WWB: Map All The groups into the values for the RouteData 
                        for (Int32 index = 0;
                            index < matchCollection[0].Groups.Count;
                            index++)
                        {
                            routeData.Values.Add(Groups[index].ToString(),
                                matchCollection[0].Groups[index]);
                        }
                    }

                    return (routeData);
                default:
                    throw (new Exception(
                        String.Format("There Multiple Matches For {0} on {1}," +
                        "which means that the regular expression has more " +
                        "then one non-overlapping match.",
                        Regex.ToString(),
                        httpContext.Request.Url.AbsoluteUri)));
            }
        }

        public override VirtualPathData GetVirtualPath(
            RequestContext requestContext,
            RouteValueDictionary values)
        {
            throw new NotImplementedException();
        }
    }
}
{6230289B-5BEE-409e-932A-2F01FA407A92}

Application_Start Only Once

Quick note about Application_Start that I noticed when working with ASP.NET MVC. It gets called only once. Which makes total sense, since the application only starts once. However, it gets run only once even if there is an exception thrown from the code within Application_Start. Which means if RegisterRoutes throws an exception, then you need to trigger the application to reset, otherwise your routes will not be registered on the next call. For Example: 1) You code a new route in RegisterRoutes. 2) Compile 3) You request a page, this calls Application_Start 4) There is an exception in your route and RegisterRoutes throws an exception. 5) You attach the debugger. 6) You request the page again to reproduce the error. This is where you notice that the error doesn't happen again, since Application_Start has already been called and attaching the debugger doesn't restart the application. Here are some of the things I know that restarts the application: 1) Edit the web.config -- just add a space. 2) Recompile the application. It would be cool if you could try catch and have the catch restart the application. Or go into an error mode if the Application_Start had an exception. {6230289B-5BEE-409e-932A-2F01FA407A92}

Tuesday, September 1, 2009

RegularExpression Routing Class for MVC

I am working with a ASP.NET WebForms website trying to convert it over to ASP.NET MVC 1.0 and need to make sure all the old URLs route correctly. We were using a combination of an HTTPHandler and Custom 404 redirects to give the web site some fancy URLs -- now I need to mimic that functionality with MVC in order not to lose the Google links that the site depends on for revenue. We really only care about the name part of the URI and the hint that it ends in .htm (the .htm extension is mapped to the ASP.NET ISAPI extension handler). So the route I tried to add looked like this:
            routes.Add(new Route(
                "{*path}/{name}.htm",
                new NameRouteHandler()));
However, I got this error message: A catch-all parameter can only appear as the last segment of the route URL. Which was an issue; the path which I tossed away had a lot of optional subdirectories which didn't map well to the MVC Route syntax. What I really wanted was to treat the routing syntax like a regular expression. To make this happened I created my Route class subclassed from RouteBase that took a regular expression. So now route declaration looks like this:
            routes.Add(new RegexRoute(
                new Regex(@".*/(.*)\.htm"),
                new String[] {"all", "name"},
                new NameRouteHandler()));
The second parameter isn't the defaults, it is the group name. Regular Expression have the concept of groups within a match and the parenthesizes in the regular expression tell what to group up -- in this case the name. With group the complete match is always the first group. The RegexRoute class trys to make the AbsoluteUri of the request with the regular expression if it can't it returns null. If it can it fills out the RouteData class and returns it. For all the groups it creates a value entry in RouteData that gets passed to the controller. The value name is the name listed in the second parameter. I haven’t implemented GetVirtualPath, which I will do as I learn more about MVC. It is used for the TDD testing to generate URLs and I am guessing will be the tricky part of this class to implement. Here is what the class looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Routing;
using System.Text.RegularExpressions;

namespace WebUtility
{
    public class RegexRoute : RouteBase
    {
        public Regex Regex { get; private set; }
        public String[] Groups { get; private set; }
        private IRouteHandler RouteHandler { get; set; }

        /// <summary>
        /// Creates A Regular Expression Route
        /// </summary>
        /// <param name="regex">Regular Expression To Use Against
        /// the AbsoluteUri of the request.</param>
        /// <param name="groups">An Array Of gorup names to use 
        /// for Value namdes of the RouteData</param>
        /// <param name="routeHandler">The name of the Handler 
        /// to use for this route.</param>
        public RegexRoute(Regex regex,
            String[] groups, 
            IRouteHandler routeHandler)
        {
            Regex = regex;
            Groups = groups;
            RouteHandler = routeHandler;
        }

        public override RouteData GetRouteData(
            System.Web.HttpContextBase httpContext)
        {
            MatchCollection matchCollection = 
                Regex.Matches(httpContext.Request.Url.AbsoluteUri);

            switch (matchCollection.Count)
            {
                case 0:
                    // WWB: There Is No Match --
                    //  This Route Doesn't Handle This URI
                    return (null);
                case 1:

                    // MSDN:The GroupCollection object returned
                    //      by the Match.Groups property
                    //      always has at least one member.

                    if (matchCollection[0].Groups.Count != Groups.Length)
                        throw (new Exception(String.Format(
                            "{0} contains {1} groups when matching {2}, however " + 
                            "there are only {3} mappings.  There needs to be an " +
                            "equal number of mappings to groups, note that " +
                            "there is always one group the whole string.",
                            httpContext.Request.Url.AbsoluteUri,
                            matchCollection[0].Groups.Count,
                            Regex.ToString(),
                            Groups.Length)));

                    // WWB: FillOut The Route Data
                    RouteData routeData = new RouteData();
                    routeData.Route = this;
                    routeData.RouteHandler = RouteHandler;

                    // WWB: No Group Names, No Values Outputted.
                    if (Groups != null)
                    {
                        // WWB: Map All The groups into the values for the RouteData 
                        for (Int32 index = 0;
                            index < matchCollection[0].Groups.Count - 1;
                            index++)
                        {
                            routeData.Values.Add(Groups[index].ToString(),
                                matchCollection[0].Groups[index + 1]);
                        }
                    }

                    return (routeData);
                default:
                    throw (new Exception(
                        String.Format("There Multiple Matches For {0} on {1}," +
                        "which means that the regular expression has more " +
                        "then one non-overlapping match.", 
                        Regex.ToString(),
                        httpContext.Request.Url.AbsoluteUri)));
            }
        }

        public override VirtualPathData GetVirtualPath(
            RequestContext requestContext,
            RouteValueDictionary values)
        {
            throw new NotImplementedException();
        }
    }
}
{6230289B-5BEE-409e-932A-2F01FA407A92}