jBlogMvc : part 3 Themable View Engine, Archive and Filtering by year, month and day

NOTE: In this series I build a blogengine using ASP.NET MVC and jQuery from scratch in order to learn more about these new technologies. If you haven’t read the first post in this series, I would encourage you do to that first, or check out the jBlogMvc category. You can also always subscribe to the feeds.

jBlogMvc is converted to be used on ASP.NET MVC Beta 1 if you haven’t downloaded it yet you can find it here, to read about the changes and additions in the beta 1 I do recommend reading ScottGu’s asp net mvc beta1 announcement if you haven’t already read it.

This part had witnessed a big change in project structure hence the new release of the beta1 and building a themable folder structure as shown in the pic.solutionExplorer  [more]

I built a ThemableWebFormViewEngine which now is responsible to find and create the Views to be rendered, the following listing shows the Theme View Engine

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Globalization;

namespace jBlogMvc.Utils
{
    public class ThemableWebFormViewEngine : WebFormViewEngine
    {
        public ThemableWebFormViewEngine()
        {
            base.ViewLocationFormats = new string[] {
                "~/Themes/{2}/{0}.aspx",
                "~/Themes/{2}/{0}.ascx",
                "~/Views/{1}/{0}.aspx",
                "~/Views/{1}/{0}.ascx",
                "~/Views/Shared/{0}.ascx",
                "~/Views/Shared/{0}.aspx"
            };

            base.MasterLocationFormats = new string[] {
                "~/Themes/{2}/{0}.master",
                "~/Views/{1}/{0}.master"
            };

            base.PartialViewLocationFormats = ViewLocationFormats;
        }

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (string.IsNullOrEmpty(viewName))
            {
                throw new ArgumentException("Value is required.", "viewName");
            }

            string themeName = GetTheme(controllerContext);

            string[] searchedViewLocations;
            string[] searchedMasterLocations;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");

            string viewPath = this.GetPath(this.ViewLocationFormats, viewName, controllerName, themeName, out searchedViewLocations);
            string masterPath = this.GetPath(this.MasterLocationFormats, viewName, controllerName, themeName, out searchedMasterLocations);

            if (!(string.IsNullOrEmpty(viewPath)) && (!(masterPath == string.Empty) || string.IsNullOrEmpty(masterName)))
            {
                return new ViewEngineResult(this.CreateView(controllerContext, viewPath, masterPath), this);
            }
            return new ViewEngineResult(searchedViewLocations.Union<string>(searchedMasterLocations));
        }

        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (string.IsNullOrEmpty(partialViewName))
            {
                throw new ArgumentException("Value is required.", partialViewName);
            }

            string themeName = GetTheme(controllerContext);

            string[] searchedLocations;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");

            string partialPath = this.GetPath(this.PartialViewLocationFormats, partialViewName, controllerName, themeName, out searchedLocations);

            if (string.IsNullOrEmpty(partialPath))
            {
                return new ViewEngineResult(searchedLocations);
            }
            return new ViewEngineResult(this.CreatePartialView(controllerContext, partialPath), this);
        }

        private string GetTheme(ControllerContext controllerContext)
        {
            string theme = controllerContext.HttpContext.Request.QueryString["theme"];
            if (controllerContext.RouteData.Values["Action"].ToString() == "ThemePreview" &&
             !string.IsNullOrEmpty(theme))
            {
                return theme;
            }
            else return Config.Instance.Theme;
        }

        private string GetPath(string[] locations, string viewName, string controllerName, string themeName, out string[] searchedLocations)
        {
            string path = null;

            searchedLocations = new string[locations.Length];

            for (int i = 0; i < locations.Length; i++)
            {
                path = string.Format(CultureInfo.InvariantCulture, locations[i], new object[] { viewName, controllerName, themeName });
                if (this.VirtualPathProvider.FileExists(path))
                {
                    searchedLocations = new string[0];
                    return path;
                }
                searchedLocations[i] = path;
            }
            return null;
        }

    }
}

This code is based on the work Chris Pietschmann of here.

The theme folder should contain the following views

  1. site.master “the overall look and feel”
  2. Index.aspx “for multi posts page”
  3. archive.aspx “archive page”
  4. login.aspx “login page”
  5. single.aspx “single post page”
  6. _postview.acsx “for the post template”

I also added in the Blog General Settings an option list for querying all themes available.

I also added a ThemePreview action which you can preview how themes look with your posts without applying it, you can test the theme using a url like this http://localhost:2113/themepreview?theme=Transparentia as you can see in the code above line 85, the method that decides which theme to render check first if the action themepreview is used and if there is a theme parameter in the query string.

 

Aslo jBlogMvc now supports the Archive page, when the posts get more and more readers like to have a page that has all the posts, and this is the description of the archive page, with an action like the following,

public ActionResult Archive()
{
    var posts = _repository.GetPostList();
    return View(posts);
}

and a simple view, that renders a table of posts titles and dates.

<%@ Page Title="" Language="C#" MasterPageFile="~/Themes/Indigo/Site.Master" AutoEventWireup="true" CodeBehind="Archive.aspx.cs" Inherits="jBlogMvc.Themes.Indigo.Archive" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    <h1>Archive</h1>
    <div class="item">
    <%if (ViewData.Model == null || ViewData.Model.Count <= 0)
      {%>
    <h2>No Posts published yet.</h2>
    <% }
      else{%>
    <table width="100%">
    <thead><tr><th align="left">Title</th><th >Date</th></tr></thead>
    <tbody>
    <%
        foreach (var post in ViewData.Model)
        {%>
            <tr>
            <td style="width:80%"><h3><a href="<%=post.RelativeLink %>"><%=post.Title%></a></h3></td>
            <td style="width:20%" align="right"><h3><%=post.CDate.ToString("dd.MMM yyyy")%></h3></td>
            </tr>
        <%}
    %>
    </tbody></table>
    <%} %>
    </div>
</asp:Content>

Filtering Posts by Date using url

Also I added date filtering in url so that you can query posts by date like this

http://localhost:2113/posts/2008/9/24

Or

http://localhost:2113/posts/2008/9

Or

http://localhost:2113/posts/2008/

This was done by adding the following route in the route table at application start

routes.MapRoute(
    "Calendar",
    "posts/{year}/{month}/{day}",
    new { controller = "Home", action = "Index", id = "", year = "", month = "", day = "" }
);

 

Some Changes

PostBinder class is no longer used, I grabbed this part from Scott’s announcement

Preview 5 introduced the concept of “model binders” – which allow you to map incoming form post values to complex .NET types passed as Controller action method parameters.  Model binders in preview 5 were extensible, and you could create custom binders and register them at multiple levels of the system.  Preview 5 didn’t ship with any “pre-built” binders, though, that you could use out of the box (you instead had to build your own).  Today’s beta now includes a built-in, pre-registered, binder that can be used to automatically handle standard .NET types – without requiring any additional code or registration.

You can see how the AddPost action in the AdminController accepts a Post parameter just as before now with now binders, however, you can see me using Bind attribute on the post with a parameter Prefix so why?, the out of box implementation will use parameter name “p” in this case and find in the form post collection for p.body, p.title, p.slug and so on, so the developer can override this default behavior by using this attribute, here I am sending an empty prefix so it should find form post variables named body, title and so on.

public ActionResult AddPost([Bind(Prefix="")]Post p)
{
    if (!ViewData.ModelState.IsValid)
        return View("WritePost", p);

    try
    {
        _repository.InsertPost(p);
        return RedirectToRoute("Posts", new { slug = p.Slug });
    }
    catch
    {
        Helpers.UpdateModelStateWithViolations(p, ViewData.ModelState, System.Data.Linq.ChangeAction.Insert);
        return View("WritePost", p);
    }
}

Summary

So what do you think? you are most welcomed to leave comments.

Download version one : jBlogMvc_version_3.zip

If you liked this blog post then please subscribe to this blog.


Posted

in

,

by

Tags:

Comments

One response to “jBlogMvc : part 3 Themable View Engine, Archive and Filtering by year, month and day”

  1. Ayman El-Hattab Avatar

    3amooor,
    I wish to read those posts, they seem very useful to the community but unfortunately, NO TIME !
    Yalla c u

Leave a Reply

Your email address will not be published. Required fields are marked *