pr0g33k

 collapse all
  1. Encode/Decode a URL for use as a Route Parameter

    I've been working on integrating this blog with Microsoft's Azure Blob Storage. I'll have more posts on how to set it up in a few days. In the mean time, I found a nice, little gem I thought I'd share.

    I have a page in the admin section of this blog with a controller and views for file management (List.cshtml, Upload.cshtml, Edit.cshtml, etc.). In the List.cshtml view, I have an unordered list driven by an IEnumerable<IListBlobItem> queried from the Azure CloudBlobContainer. For each URI in IListBlobItem, I create an ActionLink that navigates to Edit.cshtml. I needed to pass the URI as a RouteValue, though, and I was pretty sure that the following wouldn't work very well: http://localhost:1111/FileManager/Edit/http://localhost:10000/images/my-uploaded-image.jpg

    HttpServerUtility.UrlTokenEncode(Byte[] input) & HttpServerUtility.UrlTokenDecode(String input) to the Rescue!

    @Html.ActionLink(Path.GetFileName(url), "Edit", new { id = HttpServerUtility.UrlTokenEncode(System.Text.UTF8Encoding.ASCII.GetBytes(url)) }, new { data_href = url })

    That creates a link that renders like this:

    <a data-href="http://127.0.0.1:10000/devstoreaccount1/images/my-uploaded-image.jpg" href="/Admin/FileManager/Edit/aHR0cDovLzEyNy4wLjAuMToxMDAwMC9kZXZzdG9yZWFjY291bnQxL2ltYWdlcy8xcXZnZnV5bmJrYW1xZ2F6M2pkaTZ3Mi5qcGc1">my-uploaded-image.jpg</a>

    In the FileManager controller, the Edit method accepts the id RouteParameter and decodes the value:

    public ActionResult Edit(String id)
    {
        ViewBag.Url = UTF8Encoding.ASCII.GetString(HttpServerUtility.UrlTokenDecode(id));
        return View();
    }
    

    The "data-href" attribute is used by a jQuery script that opens a preview popup if the file is an image. I'll cover that in a future post.

    Posted on 4/25/2013 at 02:04 PM , Edited on 4/25/2013 at 08:04 PM
    Tags: C#MVC
  2. Slugging it out with your URLs

    I've been told that slugs are the very first thing read by search engines. They're that important.

    To some, that statement sounds silly. You might be asking, "What does the common shell-less terrestrial gastropod mollusk have to do with the Internet?" Well, I'm not talking about that kind of slug. I'm talking about those pretty, easy-to-read URL's you see these days. If you clicked through to the specific article of this blog post (click on the title if you haven't already), the URL should be http://www.pr0g33k.com/blog/2/slugging-it-out-with-your-urls. The "slugging-it-out-with-your-urls" part is a slug.

    These days, content is king when it comes to search engine optimization (SEO). The more concise and relevent your content, the better chances you have of getting a higher ranking. Just as the title tag is important for on-site optimization, so are slugs. This is especially true because people will actually read the URL these days to determine if they want to visit a particular site. How many times have you searched for something like "best italian food in dallas, tx" and the first link in the results said "Mama Mia!" but the URL slug said, "20-things-not-to-say-to-your-wifes-face"? You probably wouldn't bother clicking through, right?

    To create slugs, I wrote a very simple extension method to System.String called "ToSlug()." Here's the code:

    public static String ToSlug(this String s)
    {
        return Regex.Replace(Regex.Replace(Regex.Replace(s, @"[^a-zA-Z0-9\s-]", String.Empty), @"[\s-]+", " ").Trim(), @"\s", "-");
    }
    

    To use it, just add a using statement for whatever namespace is necessary (I put mine in a separate assembly - Core.Web - and reference it in a using statement as "Core.Web.Extensions"). To get the links in this blog to format with slugs, I do this:

    <h1>@Html.ActionLink(blog.Title, "Article", "Blog", new { id = blog.Id, slug = blog.Title.ToSlug() }, new { title = String.Format("Permalink to {0}", blog.Title), rel = "Bookmark" })</h1>
    

    I hope this helps!

    Posted on 4/18/2013 at 11:04 PM , Edited on 4/19/2013 at 12:04 AM
    Tags: MVCC#SEO
  3. Building a Tag Cloud using MVC, SQL, HTML5, and jQuery

    One of the things I definitely wanted to do when I rebuilt this blog was implement a tag cloud. To make blogging easy, I just wanted to have a comma-separated list of tags that I attached to each blog post. To get the list of tags and their occurrences, I used the following SQL stored procedure:

    Create Procedure [dbo].[Blog_GetTagsForCloud]
    As
    Begin
    	Set NoCount On
    
    	Select LTRIM(RTRIM(T.Value)) As Tag
    		, COUNT(*) As TagCount
    	From dbo.Blog
    		Cross Apply dbo.fn_ParseDelimitedStrings(dbo.Blog.Tags, ',') As T
    	Where dbo.Blog.IsActive = 1
    	Group By LTRIM(RTRIM(T.Value))
     	Order By NEWID()
    End
        

    The fn_ParseDelimitedStrings table-valued function is used in a Cross Apply so that the Tags column can be evaluated for each record in the Blog table. Grouping on the function's Value column gives us a distinct list of tags and in conjunction with the COUNT(*) function, we get a total count of each tag. The "Order By NEWID()" is there to randomize the order of the tags (as random as it can be, anyway). Here's the fn_ParseDelimitedStrings function:

    Create Function [dbo].[fn_ParseDelimitedStrings](@String nvarchar(MAX), @Delimiter char(1))
    Returns @Values Table 
    (
    	Id int Not Null Identity(1,1) Primary Key
    	, Value nvarchar(MAX) Not Null
    )
    As
    Begin
    	If (Right(@String, 1) != @Delimiter)
    		Set @String = @String + @Delimiter
    
    	Declare @StartPosition smallint = 1
    	Declare @EndPosition smallint = CharIndex(@Delimiter, @String)
    
    	While @EndPosition > 0
    	Begin
    		Insert @Values(Value)
    			Select LTrim(RTrim(SubString(@String, @StartPosition, @EndPosition - @StartPosition)))
    
    		Set @String = Stuff(@String, @EndPosition, 1, '')
    		Set @StartPosition = @EndPosition
    		Set @EndPosition = CharIndex(@Delimiter, @String)
    	End
    
    	Return
    End
        

    Once I have the distinct list of tags and their count, I use a nifty little formula to figure out their proportions and ratios compared to the proportions and ratios of the font sizes I want to use. The formula looks like this:

    Font Size = ((([the count of the tag being computed] - [the lowest occurrence]) * ([the largest font size] - [the smallest font size])) / ([the highest occurrence] - [the lowest occurrence])) + [the smallest font size]

    Simple, right? ;^) Translated to C#, it looks like this:

    public Dictionary<String, Int32> GetTagsForCloud()
    {
        Dictionary<String, Int32> items = new Dictionary<String, Int32>();
    
        using (SqlDataReader reader = GetDataReader("dbo.Blog_GetTagsForCloud"))
        {
            while (reader.Read())
                items.Add(Convert.ToString(reader["Tag"]), Convert.ToInt32(reader["TagCount"]));
    
            reader.Close();
        }
    
        Dictionary<String, Int32> tagCloud = new Dictionary<String, Int32>();
    
        if (items.Count > 0)
        {
            Int32 minValue = items.Min(kvp => kvp.Value);
            Int32 maxValue = items.Max(kvp => kvp.Value);
            Int32 divisor = maxValue - minValue;
            Int32 minFont = 12;
            Int32 maxFont = 36;
    
            if (divisor == 0)
                divisor = 1;
    
            foreach (var item in items)
                tagCloud.Add(item.Key, (((item.Value - minValue) * (maxFont - minFont)) / divisor) + minFont);
        }
    
        return tagCloud;
    }
        

    I have a data abstraction layer and that's where the GetTagsForCloud() function resides. The ORM I use maps column names (or aliases) to objects but since this is a little outside my object structure, I just grab a SqlDataReader and iterate it.

    Note that I had to use the "divisor" variable to counter the possibility of minValue and maxValue being the same. That would result in a divide-by-zero error.

    Next I created a partial view (_TagCloud.cshtml) and put it in the Views/Shared folder since I use it in my _Layout.cshtml:

    @inherits RobertGaut.Pr0g33k.Web.Views.Shared.TagCloudView
    <div id="tag-cloud">
        @foreach (var tag in TagCloud)
        {
            @Html.ActionLink(tag.Key, "Index", new { tag = tag.Key }, new { data_font_size = String.Format("{0}", tag.Value), title = String.Format("View all posts tagged with '{0}'", tag.Key) })
        }
    </div>
        

    And, since I just feel funny about putting C# in my Views, I had the partial inherit from this class:

    public abstract class TagCloudView : WebViewPage
    {
        public Dictionary<String, Int32> TagCloud { get; set; }
    
        protected override void InitializePage()
        {
            base.InitializePage();
            TagCloud = BlogManager.Instance.GetTagsForCloud();
        }
    }
        

    I marked the class "abstract" so that I didn't have to implement the WebPageView's Execute() method. I initially tried to put the call to GetTagsForCloud() in the Execute() method but the timing wasn't right so I moved it to an earlier call, InitializePage(), and the data came through to my partial view just fine.

    Now I had the problem of figuring out how to apply the font size to the hyperlink. I try to avoid using inline styles with HTML5. People keep telling me that's soooo XHTML Transitional 1.0 so I just avoid it altogether. But I definitely didn't want to have to do it with a class attribute and define 24 class selectors! Let me explain. If you look back up at the GetTagsForCloud() method, you'll notice that I define 2 variables:

    Int32 minFont = 12;
    Int32 maxFont = 36;
        

    I'm using them in the formula to make a ratio comparison to the current occurrence and the minimum occurrence. Basically, I'm setting a range from 12 to 36 which I'll translate to "12px" to "36px." If I used a style selector for each size, they'd look something like this:

    .font12 {
        font-size: 12px;
    }
    
    /* the in-between definitions would go here if I wasn't too lazy to type them */
    
    .font36 {
        font-size: 36px;
    }
        

    That's just plain nasty. So instead, I added the value to an HTML5 "data-" attribute. Check out the _TagCloud.cshtml partial view and you'll notice the overload for htmlAttributes has the following:

    data_font_size = String.Format("{0}", tag.Value)
        

    If you tried to type it as "data-font-size" the way it is done in the actual HTML element, you'd get red squigglies telling you that that's no bueno – you can't use hyphens there because it needs to translate the word to a variable name when Razor parses/compiles the statement. By using underscores, though, the Razor view engine outputs the attribute with hyphens. Which, I thought, is pretty darn sweet. Now that I have anchor elements output to the page I can use the following jQuery script to set the font size using the data-font-size attribute.

    <script>
        $(document).ready(function () {
            $('#tag-cloud > a').each(function () {
                $(this).css({ 'font-size': $(this).attr('data-font-size') + 'px' });
            });
        });
    </script>
        

    UPDATE

    Alternatively, you can output the tag cloud using @Html.RenderAction(). I don't know why I didn't do this in the first place. Anyway, here's what the controller looks like:

    public class SharedController : Controller
    {
        public ActionResult TagCloud()
        {
            return PartialView(BlogManager.Instance.GetTagsForCloud());
        }
    }
        

    I put it in a "shared" controller because, well, _Layout.cshtml doesn't really have a controller associated with it. The partial view looks like this:

    @model System.Collections.Generic.Dictionary<String, Int32>
    <div id="tag-cloud">
        @foreach (var tag in Model)
        {
            @Html.ActionLink(tag.Key, "Index", new { tag = tag.Key }, new { data_font_size = String.Format("{0}", tag.Value), title = String.Format("View all posts tagged with '{0}'", tag.Key) })
        }
    </div>
        

    In _Layout.cshtml, I replaced the call to @{ Html.Partial("_TagCloud"); } with @{ Html.RenderAction("TagCloud", "Shared"); }

    This is a much better solution, IMHO.