Wednesday, December 19, 2012

Implementing a Tree View - Small Case Study

Implementing the control that allows navigating my blog history could be roughly divided into 4 steps.

1. Select and group posts from the database

Here the LINQ grouping came handy. Starting with grouping posts by year published to create my top level in hierarchy, the query would look like this:

var results = from allPosts in db.Posts.OrderBy(p => p.DateCreated)
     group allPosts by allPosts.DateCreated.Year into postsByYear;

Here results is the enumeration of groups - in my case, groups of posts which were published in the certain year. Posts are grouped by the key, which is defined in the IGrouping interface.

Moving further, I want to create child groups, in my case - posts by the month. I have to add a nested query along these lines

var results = from allPosts in db.Posts.OrderBy(p => p.DateCreated)
     group allPosts by allPosts.DateCreated.Year into postsByYear

     select new
     {
      postsByYear.Key,
      SubGroups = from yearLevelPosts in postsByYear
         group yearLevelPosts by yearLevelPosts.DateCreated.Month into postsByMonth;
     };

This is still not too bad. The first level are posts by year. Each year has SubGroups property which stores the group of posts published in a certian month. Now I finally need to get all the posts published in a month. I end up with the following query:

var results = from allPosts in db.Posts.OrderBy(p => p.DateCreated)
     group allPosts by allPosts.DateCreated.Year into postsByYear

     select new
     {
      postsByYear.Key,
      SubGroups = from yearLevelPosts in postsByYear
         group yearLevelPosts by yearLevelPosts.DateCreated.Month into postsByMonth
         select new
         {
          postsByMonth.Key,
          SubGroups = from monthLevelPosts in postsByMonth
             group monthLevelPosts by monthLevelPosts.Title into post
             select post
         }
     };

It is fully functional and suits my purposes. It is on the edge of being unreadable, however, and if I had to add one more level of depth it would probably be beyond. Following the example from Mitsu Furuta's blog, I make the query generic. The GroupResult class holds the grouping key and the group items. The GroupByMany extension allows for an undefined number of group selectors. This is the code I need to make it work:

public static class MyEnumerableExtensions
{
 public static IEnumerable<GroupResult> GroupByMany<TElement>(this IEnumerable<TElement> elements, params Func<TElement, object>[] groupSelectors)
 {
  if (groupSelectors.Length > 0)
  {
   var selector = groupSelectors.First();

   //reduce the list recursively until zero
   var nextSelectors = groupSelectors.Skip(1).ToArray();
   return
    elements.GroupBy(selector).Select(
     g => new GroupResult
     {
      Key = g.Key,
      Items = g,
      SubGroups = g.GroupByMany(nextSelectors)
     });
  }
  else
   return null;
 }
}

public class GroupResult
{
 public object Key { get; set; }
 public IEnumerable Items { get; set; }
 public IEnumerable<GroupResult> SubGroups { get; set; }
}

And now I can rewrite my query in one line:

var results = db.Posts.OrderBy(p => p.DateCreated).GroupByMany(p => p.DateCreated.Year, p => p.DateCreated.Month);

2. Populate a tree structure that will be used to generate HTML

I used a complete solution suggested by Mark Tinderhold almost without changes.

The BlogEntry class has a Name, which will be rendered, and references to Children and Parent nodes.

public class BlogEntry : ITreeNode<BlogEntry>
{
 public BlogEntry()
 {
  Children = new List<BlogEntry>();
 }

 public string Name { get; set; }
 public BlogEntry Parent { get; set; }
 public List<BlogEntry> Children { get; set; }
}

A list of BlogEntry is populated from my query results

var entries = new List<BlogEntry>();

//years
foreach (var yearPosts in results)
{
 //create "year-level" item
 var year = new BlogEntry { Name = yearPosts.Key.ToString().ToLink(string.Empty) };
 entries.Add(year);

 //months
 foreach (var monthPosts in yearPosts.SubGroups)
 {
  var month = new BlogEntry { Name = new DateTime(2000, (int)monthPosts.Key, 1).ToString("MMMM").ToLink(string.Empty), Parent = year };
  year.Children.Add(month);

  foreach (var postEntry in monthPosts.Items)
  {
   //create "blog entry level" item
   var post = postEntry as Post;
   var blogEntry = new BlogEntry { Name = post.Title.ToLink("/Post/" + post.PostID + "/" + post.Title.ToSeoUrl()), Parent = month };
   month.Children.Add(blogEntry);
  }
 }
}

3. Use the tree structure to generate HTML

The TreeRenderer writes out HTML.

public interface ITreeNode<T>
{
 T Parent { get; }
 List<T> Children { get; }
}

public static class TreeRenderHtmlHelper
{
 public static string RenderTree<T>(this HtmlHelper htmlHelper, IEnumerable<T> rootLocations, Func<T, string> locationRenderer) where T : ITreeNode<T>
 {
  return new TreeRenderer<T>(rootLocations, locationRenderer).Render();
 }
}
public class TreeRenderer<T> where T : ITreeNode<T>
{
 private readonly Func<T, string> locationRenderer;
 private readonly IEnumerable<T> rootLocations;
 private HtmlTextWriter writer;
 public TreeRenderer(IEnumerable<T> rootLocations, Func<T, string> locationRenderer)
 {
  this.rootLocations = rootLocations;
  this.locationRenderer = locationRenderer;
 }
 public string Render()
 {
  writer = new HtmlTextWriter(new StringWriter());
  RenderLocations(rootLocations);
  return writer.InnerWriter.ToString();
 }
 /// <summary>
 /// Recursively walks the location tree outputting it as hierarchical UL/LI elements
 /// </summary>
 /// <param name="locations"></param>
 private void RenderLocations(IEnumerable<T> locations)
 {
  if (locations == null) return;
  if (locations.Count() == 0) return;
  InUl(() => locations.ForEach(location => InLi(() =>
  {
   writer.Write(locationRenderer(location));
   RenderLocations(location.Children);
  })));
 }
 private void InUl(Action action)
 {
  writer.WriteLine();
  writer.RenderBeginTag(HtmlTextWriterTag.Ul);
  action();
  writer.RenderEndTag();
  writer.WriteLine();
 }
 private void InLi(Action action)
 {
  writer.RenderBeginTag(HtmlTextWriterTag.Li);
  action();
  writer.RenderEndTag();
  writer.WriteLine();
 }
}

The renderer will be called the following way from the view:

<div id="treeview" class="treeview">
    @MvcHtmlString.Create(Html.RenderTree(Model.BlogEntries, x => x.Name))
</div>

4. Render the HTML on the webpage

After reviewing a couple of other options, I decided on a jsTree. It has rich capabilities, but to this point I only used the "default" options. I added the tree to the _Layout.cshtml by adding a line of code

@Html.Action("BlogResult", "BlogEntry")

This line calls the function in the BlogEntryController

public PartialViewResult BlogResult()
{
 var results = db.Posts.OrderBy(p => p.DateCreated).GroupByMany(p => p.DateCreated.Year, p => p.DateCreated.Month);

 entries = new List<BlogEntry>();
 
 //code that populates entries - see above

 BlogEntryViewModel model = new BlogEntryViewModel(entries);

 return PartialView(model);
}

The BlogEntryViewModel is extremely simple.

public class BlogEntryViewModel
{
 public List<BlogEntry> BlogEntries { get; set; }

 public BlogEntryViewModel(List<BlogEntry> blogEntries)
 {
  BlogEntries = blogEntries;
 }

 public BlogEntryViewModel()
 {
 }
}

Finally, the partial view that is rendered

@model Recipes.ViewModels.BlogEntryViewModel

@{ Layout = null; }

<link href="@Url.Content("~/Content/blogentry.css")" rel="stylesheet" type="text/css" />

<!-- Tree View jstree -->
<script src="@Url.Content("~/Scripts/jquery.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.hotkeys.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.cookie.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.jstree.js")" type="text/javascript"></script>

<script type="text/javascript">
    jQuery(function ($) {
        $("#treeview").jstree({ "plugins": ["themes", "html_data"] });
    });
</script>

<div class="blogheader">
<h2>Blog Archives</h2>
</div>
<div id="treeview" class="treeview">
    @MvcHtmlString.Create(Html.RenderTree(Model.BlogEntries, x => x.Name))
</div>

What I had to make sure of to make it work:

And for information, this is the contents of blogentry.css

div.treeview, div.blogheader {
    width: 14em;
    background: #eee;
   overflow: hidden;
 text-overflow: ellipsis;
}

div.blogheader h2 
{
    font: bold 11px/16px arial, helvetica, sans-serif;
    display: block;
    border-width: 1px;
    border-style: solid;
    border-color: #ccc #888 #555 #bbb;
    margin: 0;
    padding: 2px 3px;
    
    color: #fff;
    background: #000;
    text-transform: uppercase;
}

The end result looks like that:

Resulting TreeView

Resulting Treeview

References:

How can I hierarchically group data using LINQ?
Playing with Linq grouping: GroupByMany ?
Rendering a tree view using the MVC Framework
jQuery Treeview Plugin Demo
jsTree – Part 1: Introduction
jsTree on GitHub
Best place to learn JStree
by . Also posted on my website

Sunday, December 9, 2012

SEO Basics: Friendly URLs

Implementing SEO-friendly URLs turned out to be much easier than I expected - MVC routing already takes care of the "heavy lifting". The developer only needs to provide a function that returnes the "friendly urls" from strings (product names, blog titles etc.) and to update action links.

1. Routing. A new route needs to be added. It has to be added above the default route so that MVC framework attempted to match it first. The seofriendly parameter can be pretty much anything that will satisfy valid url requirements.

routes.MapRoute(
 name: \"SEOFriendly\",
 url: \"{controller}/{action}/{id}/{seofriendly}\",
 defaults: new { controller = \"Home\", action = \"Index\", id = UrlParameter.Optional, seofriendly = \"\" }
);

2. Creating friendly urls. Here is an example I found on the web and added it "as is".

public static string ToSeoUrl(this string url)
{
 // make the url lowercase
 string encodedUrl = (url ?? \"\").ToLower();

 // replace & with and
 encodedUrl = Regex.Replace(encodedUrl, @\"\&+\", \"and\");

 // remove characters
 encodedUrl = encodedUrl.Replace(\"'\", \"\");

 // remove invalid characters
 encodedUrl = Regex.Replace(encodedUrl, @\"[^a-z0-9]\", \"-\");

 // remove duplicates
 encodedUrl = Regex.Replace(encodedUrl, @\"-+\", \"-\");

 // trim leading & trailing characters
 encodedUrl = encodedUrl.Trim('-');

 return encodedUrl;
}  

3. Making use of the friendly url. Just adding an extra parameter to the object.

Before:

<div class=\"display-button\">@Html.ActionLink(\"Edit\", \"Edit\", new { id=item.PostID }) </div>
<div class=\"display-button\">@Html.ActionLink(\"Details\", \"Details\", new { id = item.PostID }) </div>
<div class=\"display-button\">@Html.ActionLink(\"Delete\", \"Delete\", new { id = item.PostID }) </div>

After:

<div class=\"display-button\">@Html.ActionLink(\"Edit\", \"Edit\", new { id=item.PostID, seofriendly = item.Title.ToSeoUrl() }) </div>
<div class=\"display-button\">@Html.ActionLink(\"Details\", \"Details\", new { id = item.PostID, seofriendly = item.Title.ToSeoUrl() }) </div>
<div class=\"display-button\">@Html.ActionLink(\"Delete\", \"Delete\", new { id = item.PostID, seofriendly = item.Title.ToSeoUrl() }) </div>

References:

SEO-Friendly URLs in ASP.Net MVC 3
How can I create a friendly URL in ASP.NET MVC?
by . Also posted on my website

Sunday, December 2, 2012

SEO Basics: Linking My Content to Google+ Using rel='author'

I've learned the first step of using rich snippets to make links to my content look better in search results. The process is not extremely complicated, but it also is not intuitive to me, so I'd better write it down. I linked the content on my Blogger blog and also on my website which I'm using as training grounds. There are several steps involved - I need to modify my Google+ account, and I need to modify the content where I publish it.

1. Google+ account.

Assuming I already have a Google+ profile with photo on it, I go to my Profile, About and select Edit Profile.

Edit Profile

I scroll down to where Contributor to section is. In there I add the places I'm going to post my content. I edit this section to specify where my content is posted. Now Google+ knows where I'm posting, but that's not enough - I have to provide a way to verify that it's actually me.

Edit Contributor

2. My Website.

Here I have full control! I can experiment without fear to break things beyond repair. I did a few simple things so far: In the _Layout.cshtml, the partial view that is rendered on every page, I added the link to my Google+ account

<head>
    <link rel="author" href="https://plus.google.com/112677661119561622427/posts"/>
 ...
</head>

Additionally (optional) I modified the view that will display my posts to update the MetaKeywords and MetaDescription (see my previous post) dynamically.

@{
    ViewBag.MetaDescription = "Description of this post";
    ViewBag.MetaKeywords = "Keywords of this post";
    ViewBag.Title = Model.Title;
}

I'll add appropriate properties to the Model later, but that's beyond the scope of this post. I think that's all.

3. Blogger.

For reason I'll try to explain below, I had to add the following to the template of my blog in Blogger:

<a class='updated' expr:href='data:post.url' rel='bookmark' title='permanent link'><abbr class='updated' expr:title='data:post.timestampISO8601'><data:post.timestamp/></abbr></a>

Edit Blogger Template

I also added the same link as I did for my website - I'm not sure it's absolutely necessary though.

I'll also be adding the following to the end of my posts:

by <a title="Evgeny" rel="author" href="https://plus.google.com/112677661119561622427?rel=author" alt="Google+" title="Google+">Evgeny</a>.

With all that done I can publish this very post on my website and Blogger and then test the results.

4. Testing

Now I can test the results by entering the link to my post in the Structured Data Testing Tool. I enter the url and the tool tests the link for me.

This is the Blogger post.

Blogger - Positive Test Result

And this is my website.

Website - Positive Test Result

Finally, what would have happened if I hadn't added that bit to the Blogger template? I did not save the exact screenshot, but the error returned was "Missing required field 'Updated'" and looked similar to the image below.

Missing Required Field "Updated"

References

Warning: Missing required field "updated" in Blogger Rich Snippet Webmaster Tool [Solved]
Embrace Authorship - The importance of rel=me and rel=author on your content's SEO and Google
Rich snippets for idiots. And, er, you.
by . Also posted on my website