Checkbox Lists

By
Dave
2010
Jun
28
01:17
Posted in

Categories and tags are commonly used to classify blog posts. In order to specify the relevant categories and tags when creating and editing posts, two different approaches were taken, as shown below in Figure 1.

Checkbox list

Figure 1. Category checkbox list and comma-seperated tags

For the tags, a simple textbox can be used, with a controller action which parses a comma-seperated list. For the category list, a series of checkboxes can be rendered with the following view code:

<div>
    <% foreach (SelectListItem item in Model.CategoryList)
       {%>
    <input type="checkbox" class="checkbox" name="Categories"
        value="<%= item.Value%>"
        <%= item.Selected ? "checked=\"checked\"" : "" %> />
    <%= Html.Label(item.Text)%>
    <%} %>
</div>

In order to simplify the markup in the view, I created a simple HtmlHelper. The view code then becomes as follows:

<%= Html.CheckBoxList(Model.CategoryList, "Categories", new { Class = "checkbox" }, null)%>

The HtmlHelper is as follows:

public static string CheckBoxList(this HtmlHelper helper, List<SelectListItem> selectList,
    string name, object checkboxHtmlAttributes, object labelHtmlAttributes)
{
    StringBuilder stringBuilder = new StringBuilder();

    foreach (SelectListItem item in selectList)
    {
        TagBuilder tagBuilder = new TagBuilder("input");
        tagBuilder.MergeAttribute("type", "checkbox");
        tagBuilder.MergeAttribute("name", name);
        tagBuilder.MergeAttribute("value", item.Value);
        tagBuilder.MergeAttributes(new RouteValueDictionary(checkboxHtmlAttributes));

        if (item.Selected)
            tagBuilder.MergeAttribute("checked", "checked");

        // add checkbox
        stringBuilder.AppendLine(tagBuilder.ToString(TagRenderMode.SelfClosing));

        tagBuilder = new TagBuilder("label");
        tagBuilder.SetInnerText(item.Text);
        tagBuilder.MergeAttributes(new RouteValueDictionary(labelHtmlAttributes));

        // add label
        stringBuilder.AppendLine(tagBuilder.ToString());
    }

    return stringBuilder.ToString();
}

The model must include both the SelectList to pass the list of categories to the view, and an array of strings to post the selected values back to the controller, as follows:

public class MyModel
{
    ...
    public List CategoryList { get; set; }
    public string[] Categories { get; set; }
}

Regardless of which approach is used, the controller code looks like the following:

[HttpPost]
public ActionResult MyAction(MyModel model)
{
    if (model.Categories != null)
    {
        foreach (string item in model.Categories)
        {
            ...
        }
    }
    ...
}

Note that in both cases, only the checkboxes that are checked will have their values submitted to the controller, which is fine for this approach, as I (re)consruct the list of selected categories on each action.

Valid Comments

By
Dave
2010
Jun
26
23:02
Posted in

One of the trickier things I had to do for my blog engine was to support comment submission. Comments are only shown for a post when viewing the "Detail" view of a single blog post, and I wanted to show a form to submit a new comment at the bottom of this page.

In order to do this I had to use a specific ViewModel class to pass both the blog post, and a skeleton comment to the view. In this way, when the form is posted to the server, the default ModelBinder will present the controller with the comment. However, since the blog post is not on the HTML form (it only has the comment), I have to maintain a reference to the blog post using an HTML hidden form field:

<%= Html.HiddenFor(model => model.Comment.Post.Id) %>

The normal controller pattern to use when validating and saving data is as follows:

public ActionResult MyAction(...)
{
    ...
    Post post = myServiceLayer.GetPost(...);
    return View(post);
}

[HttpPost]
public ActionResult MyAction(...)
{
    ...
    if (myServiceLayer.AddComment(comment));
    {
        return RedirectToAction("MyAction");
    }
    else
    {
        // rebuild ViewModel and return view
        return View("MyView", new MyViewModel { Post = myServiceLayer.GetPost(...), Comment = comment });
    }
}

This pattern is used such that a successful post returns a client-side redirect to a page which re-displays the view. If the user refreshes their browser page, since we've done a re-direct there will be no prompts to re-submit the HTML form. This is called Post-Redirect-Get (PRG).

What happens if the comment fails validation? In this case, I need to re-display the form with the relevant errors and submitted values. I also need to ensure that the view is scrolled to the correct location so that the Comment form is displayed. Firstly I rebuild the ViewModel using the the submitted comment and blog post id from the hidden HTML field. Next, in order to return the ViewModel and ModelState, which contains the validation errors, I need to return a view. The only way I can find to specify a HTML anchor tag for this view is to include a HTML Action property on the HTML form in the view itself:

<div id="MyAnchor">
    MyTitle</div>
<%= Html.ValidationSummary(true, "...") %>
<% using(Html.BeginForm("MyAction", "MyController", FormMethod.Post,
    new { Action = string.Format("{0}#MyAnchor", Request.Url.AbsoluteUri) })) %>

A screenshot is shown below in Figure 1.

Blog comment validation

Figure 1. Blog comment validation.

I could have used AJAX to do this and avoid the problem of having to scroll the form, however I would still have to provide downlevel support for browsers with Javascript disabled.

A New Blog

By
Dave
2010
Jun
21
23:04
Posted in

I finally decided to switch to using my own blog engine. There were several reasons for this:

  1. I was getting too much spam with my existing engine
  2. I wanted complete control over rendering
  3. But really...I needed an excuse to learn the ASP.NET MVC2 Framework! :)

I didn't need anything particularly fancy, and the following requirements would initially suffice:

  1. Single author, single blog
  2. View posts, with filtering by category, tag, year, month, day, and name
  3. Paging on post views
  4. Submit comments
  5. View comments with post detail
  6. Author sign-in to create & edit posts, edit & delete comments
  7. Syndication
  8. Recent posts, archive list, tag cloud, blogroll

Going forward, the following were good candidate next-steps:

  1. Live-writer support
  2. SEO
  3. Further feed-format support for ATOM
  4. Mobile support

Storage

I decided to stick with XML files for the time-being, mainly since my previous engine used them and it would save having to migrate the content to a database. Going forward, a database would clearly be a more scalable option, but it will be a while before I generate enough content for this to be an issue.

Syndication

I used the WCF SyndicationFeed and SyndicationItem classes to build an RSS 2.0 feed, and returned the feed from my controller as a derived ActionResult, using an approach outlined in this post on Joe Wardell's blog, and shown below in Listing 1.

public class RssResult : ActionResult
{
    public SyndicationFeed Feed { get; set; }

    public RssResult() { }

    public RssResult(SyndicationFeed feed)
    {
        this.Feed = feed;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.ContentType = "application/rss+xml";
        Rss20FeedFormatter formatter = new Rss20FeedFormatter(this.Feed);
        using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.Output))
        {
            formatter.WriteTo(writer);
        }
    }
}

Listing 1. Returning an RSS feed as a derived ActionResult

URL Structure

Backwards-compatibility with any links to the existing blog were something I needed to consider. An appropriate route specification should ensure compatibility, but at the expense of having to use .aspx extensions.

Looking at popular blog engines such as WordPress and MSDN, I decided to use the following scheme:

  1. http://{domain}/blog/ for all posts
  2. http://{domain}/blog/arhive/{year} for all posts in a given year
  3. http://{domain}/blog/arhive/{year}/{month} for all posts in a given month
  4. http://{domain}/blog/arhive/{year}/{month}/{day} for all posts in a given day
  5. http://{domain}/blog/arhive/{year}/{month}/{day}/{name} for a specific post
  6. http://{domain}/blog/category/{name} for posts in a given category
  7. http://{domain}/blog/tag/{name} for posts with a given tag
  8. http://{domain}/blog/feed for an RSS 2.0 feed

Posts are paged and ordered chronologically with newest posts first.

I'm hoping to switch in a couple of days, at which point any feed subscriptions will need updating. Apologies for any inconvenience.