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.

jBlogMvc : part 2 Editing, Deleting, Paging Posts and Rss feeds

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.

What about new features this part will cover :

  1. Configuration is saved in the database.
  2. Managing Posts (Editing, Deleting).
  3. Posts are now paged.
  4. Some jquery magic is used.

So, lets have a tour in the project one more time. [more]

Database

Database has now a new table to read and write the blog settings.

The project design has changed I applied the Repository Pattern (as recommended in some feedback) , so know I have an extra layer I don’t plan on supporting other data stores but its a good practice (anyway this series is to learn).

Helpers

Pagination is added it has been discussed many times I will not repeat the code I got over here, for more about paging in ASP.NET MVC check the following excellent posts

Models

IBlogRepository and its implementation were added to this folder, the IBlogRepository is as listed here

public interface IBlogRepository
{
    #region Posts
    Post GetPostBySlug(string slug);
    Post GetPostByPemalink(Guid premalink);
    PagedList<Post> GetPostList(int pageIndex, int pageSize);

    void InsertPost(Post p);
    void UpdatePost(Post p);
    void DeletePost(Post p);
    #endregion

    #region Settings
    void SaveSetting(Setting s);
    Setting GetSetting(string settingKey);
    #endregion
}

 

Controllers

Still having the main two controllers (Home and Admin)  but many changes have came through, due to changing the structure and using repository.

Home Controller now sends a PagedList rather an ordinary List to the View, and I added a feed action which returns rss feeds of the blog as shown below

public ActionResult Feed()
{
    XDocument document = new XDocument(
        new XDeclaration("1.0", "utf-8", null),
            new XElement("rss",
                    new XElement("channel",
                        new XElement("title", Config.Instance.BlogName),
                        new XElement("link", "http://www.northwindtraders.com"),
                        new XElement("description", Config.Instance.BlogDescription),

                        from post in _repository.GetPostList(0, Config.Instance.BlogSyndicationFeeds)
                        orderby post.CDate descending
                        select new XElement("item",
                            new XElement("title", post.Title),
                            new XElement("description", post.Body),
                            new XElement("link", Request.Url.GetLeftPart(UriPartial.Authority) + post.RelativeLink)
                            )
                       ), new XAttribute("version", "2.0")));
    StringWriter sb = new StringWriter();
    document.Save(sb);

    return Content(sb.ToString(), "text/xml", Encoding.UTF8);
}

Admin Controller has a lot of additions as shown in the code listing.

[AcceptVerbs("GET")]
public ActionResult EditPost(Guid? id)
{
    if (!id.HasValue) return RedirectToAction("ManagePosts");
    Post p = _repository.GetPostByPemalink(id.Value);
    if (p == null) return RedirectToAction("ManagePosts");
    return View(p);
}

[AcceptVerbs("POST")]
public ActionResult UpdatePost(Guid id)
{
    Post p = _repository.GetPostByPemalink(id);
    if (!ViewData.ModelState.IsValid)
        return View("ManagePosts", p);

    try
    {
        UpdateModel(p, new string[] { "Title", "Body", "Slug", "CDate" });
        _repository.UpdatePost(p);
        return RedirectToRoute("Posts", new { slug = p.Slug });
    }
    catch
    {
        Helpers.UpdateModelStateWithViolations(p, ViewData.ModelState, System.Data.Linq.ChangeAction.Update);
        return View("ManagePosts", p);
    }
}

[AcceptVerbs("GET")]
public ActionResult DeletePost(Guid? id)
{
    if (!id.HasValue) return RedirectToAction("ManagePosts");
    Post p = _repository.GetPostByPemalink(id.Value);
    if (p == null) return RedirectToAction("ManagePosts");
    return View(p);
}

[AcceptVerbs("POST")]
public ActionResult RemovePost(Guid id)
{
    Post p = _repository.GetPostByPemalink(id);
    if (!ViewData.ModelState.IsValid)
        return View("ManagePosts", p);

    try
    {
        _repository.DeletePost(p);
        return RedirectToAction("ManagePosts");
    }
    catch
    {
        Helpers.UpdateModelStateWithViolations(p, ViewData.ModelState, System.Data.Linq.ChangeAction.Insert);
        return View("ManagePosts", p);
    }
}

public ActionResult ManagePosts(int? page)
{
    var posts = _repository.GetPostList(page ?? 0, 25);
    return View(posts);
}

public ActionResult GeneralSettings()
{
    return View();
}
public ActionResult ReadingSettings()
{
    return View();
}

Views

A lot of views were added in this part 2 other nested master pages have been added Admin_Manage and Admin_Settings for managing blog content and settings respectively some content views were added too.

  1. ManagePosts : Grid for all posts.
  2. EditPost : editing a post.
  3. DeletePost : confirm deleting a post.
  4. GeneralSettings : Blog Name, Blog description.
  5. ReadingSettings : Posts per page, syndication count.

I will not copy and paste code here, please take a look at the attached project.

jQuery

This part didn’t miss some of the jQuery magic as well, I found another interesting plugin called jEditable which allows ajax inline editing, its pretty cool and small, all you need to start using it, is an Action that accepts POST verbs and returns some value.

I used it here with the (Settings) panel to read and write blog settings, the following code snippet is from the GeneralSettings.aspx view page defined in the document ready event.

 

$("#blogname").editable('<%=Url.Action("UpdateSettings","Admin") %>', {
               submit: 'ok',
               cancel: 'cancel',
               cssclass: 'editable',
               width: '99%',
               placeholder: 'emtpy',
               indicator: "<img src='../../Content/img/indicator.gif'/>"
           });

 

<p>
    <label for="blogname">Blog Name</label>
    <span class="edt" id="blogname"><%=Html.Encode(jBlogMvc.Config.Instance.BlogName)%></span>
</p>

Its clear that this code snippet assigns the textbox with id blogname to an action called UpdateSettings found in the Admin controller, shown in the next code snippet

 

[AcceptVerbs("POST")]
public ActionResult UpdateSettings(string id, string value)
{
    foreach (var item in this.GetType().GetProperties())
    {
        if (item.Name.ToLower().Equals(id, StringComparison.InvariantCultureIgnoreCase))
            item.SetValue(Config.Instance, value, null);
    }
    return Content(value);
}

inline editing

So, in the action I accept two parameters sent id and value, sent by default by the jEditable plugin which can be configured to change the variable names, the action is expecting that there is a blogsetting  in the Settings table having a key macthing the id parameter for example (blogname), which I also expect having a matching Property name in the Config class (built using the singleton pattern).

I am pretty sure that this is not the best practice for this case, thats why I am in need for constructive feedback.

Summary

And that’s all for this part, I have more and more features coming while writing this engine I have learned much till now, hope someone is learning with me too.

In this part, I used some features of the ASP.NET MVC to complete the administration area I started last, jQuery too was used to make inline editing (jEditable plugin) so what do you think? you are most welcomed to leave comments.

Download version one : jBlogMvc_version_2.zip

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