Writing an Archive Calendar, part 3

We're now at the coding climax of this series, writing the code that will actually render the calendar you see on the right.

I started off with a new class, the GraffitiCalendar (the previous calendar class, HtmlCalendar, merely rendered a read-only calendar as a standard HTML table). GraffitiCalendar is responsible for instantiating an HtmlCalendar, wiring up its two delegate properties — those responsible for generating the <a> links — and then letting it rip to produce the full "live" HTML.

  public class GraffitiCalendar {
    private int year;
    private int month;
    private PostCollection posts;

    public GraffitiCalendar(int year, int month) {
      this.year = year;
      this.month = month;
      this.posts = PostsReader.GetPostsForMonth(year, month);
    }

    public override string ToString() {
      HtmlCalendar calendar = new HtmlCalendar(year, month);
      calendar.GenerateMonthLink = GenerateMonthLink;
      calendar.GenerateDayLink = GenerateDayLink;
      return calendar.Render();
    }

    private string GetArchivePath() {
      string s = new Macros().Link("~/archive");
      if (!s.EndsWith("/"))
        s += "/";
      return s;
    }

    private string GetArchivePathForMonth(int year, int month) {
      return string.Format("{0}?year={1}&amp;month={2}", GetArchivePath(), year, month);
    }

    private string GetArchivePathForDay(int day) {
      return string.Format("{0}?year={1}&amp;month={2}&amp;day={3}", GetArchivePath(), year, month, day);
    }

    private int GetPostCount(DateTime date) {
      int count = 0;
      foreach (Post post in posts) {
        // note: early return possible because posts are ordered
        // in descending order by publish date
        if (post.Published.Date == date)
          count++;
        else if (post.Published.Date < date)
          return count;
      }
      return count;
    }

    public string GetFullMonthName(int month) {
      return new DateTime(2008, month, 1).ToString("MMMM");
    }

    public string GetAbbrMonthName(int month) {
      return new DateTime(2008, month, 1).ToString("MMM");
    }
  }

The constructor accepts the year and month for the calendar to be generated, and it stores these internally before calling PostReader to get the posts for that particular month. The ToString() method is the one that creates the HTML string by working as described above. The rest of the class shown here is merely a set of helper methods for the two delegates.

The GetArchivePath() method uses Graffiti's Macros helper class (yes, it's the same one you use in your Chalk code in your views) to convert a relative path to the archive folder into an absolute path. The two GetArchivePathXxx() methods construct paths for the month and single date archives that have the format:

https://boyet.com/archive/?year=2009&month=1

and

https://boyet.com/archive/?year=2009&month=1&day=8

We'll be seeing how to "get" at these year/month/day values later on. Note though that the links generated use &amp; for the ampersand character. Too many times developers use literal ampersands in their URLs in <a> links: this is invalid.

The two delegate methods are perhaps the most long winded methods of the whole class, but in reality, because of the work we've already done, they're trivial to write.

    public string GenerateDayLink(object sender, DayLinkEventArgs e) {
      DateTime date = new DateTime(year, month, e.Day);
      int count = GetPostCount(date);
      if (count == 0)
        return string.Empty;
      if (count == 1)
        return string.Format("<a href='{0}' title='View only post for {2}'>{1}</a>",
            GetArchivePathForDay(e.Day), e.Day, date.ToString("d-MMM-yyyy"));
      if (count == 2)
        return string.Format("<a href='{0}' title='View both posts for {2}'>{1}</a>",
            GetArchivePathForDay(e.Day), e.Day, date.ToString("d-MMM-yyyy"));
      return string.Format("<a href='{0}' title='View all {3} posts for {2}'>{1}</a>",
          GetArchivePathForDay(e.Day), e.Day, date.ToString("d-MMM-yyyy"), count);
    }

    public string GenerateMonthLink(object sender, MonthLinkEventArgs e) {
      if (e.UseFullName) {
        return string.Format("<a href='{0}' title='View all posts for {1} {2}'>{1} {2}</a>",
                             GetArchivePathForMonth(e.Year, e.Month), GetFullMonthName(e.Month), e.Year);
      }

      string format = e.ForPrevMonthLink ? "&laquo; {0}" : "{0} &raquo;";
      string text = string.Format(format, GetAbbrMonthName(e.Month));
      return string.Format("<a href='{0}' title='View all posts for {1} {2}'>{3}</a>",
          GetArchivePathForMonth(e.Year, e.Month), GetFullMonthName(e.Month), e.Year, text);
    }

I'll admit I went a little overboard on the titles for the daily links, but it looks dead cool. At the moment I'm not too happy with the monthly links format (the "full name" option is for the caption, otherwise it generates a link for the calendar footer), but what the heck.

Finally, the Chalk class itself has two public methods you can use: both overrides of ShowCalendar(). There are also four other public method you can call in order to make the archive page display work (GetPostsForMonth(), etc).

    public string ShowCalendar() {
      return ShowCalendar("", "");
    }

    public string ShowCalendar(string yearAsString, string monthAsString) {
      int year, month;
      GetDateParts(yearAsString, monthAsString, out year, out month);

      GraffitiCalendar calendar = new GraffitiCalendar(year, month);
      return calendar.ToString();
    }

    public PostCollection GetPostsForMonth(string yearAsString, string monthAsString) {
      int year, month;
      GetDateParts(yearAsString, monthAsString, out year, out month);
      return PostsReader.GetPostsForMonth(year, month);
    }

    public PostCollection GetPostsForDay(string yearAsString, string monthAsString, string dayAsString) {
      int year, month, day;
      GetDateParts(yearAsString, monthAsString, dayAsString, out year, out month, out day);
      return PostsReader.GetPostsForDate(year, month, day);
    }

    public string GetMonthDisplayName(string yearAsString, string monthAsString) {
      int year, month;
      GetDateParts(yearAsString, monthAsString, out year, out month);
      return new DateTime(year, month, 1).ToString("MMMM yyyy");
    }

    public string GetDateDisplayName(string yearAsString, string monthAsString, string dayAsString) {
      int year, month, day;
      GetDateParts(yearAsString, monthAsString, dayAsString, out year, out month, out day);
      return new DateTime(year, month, day).ToLongDateString();
    }

    private int ConvertDatePart(string valueAsString, int defValue, int low, int high) {
      int value;
      if (string.IsNullOrEmpty(valueAsString) || !int.TryParse(valueAsString, out value))
        return defValue;
      if (low <= value && value <= high)
        return value;
      return defValue;
    }

    private void GetDateParts(string yearAsString, string monthAsString, out int year, out int month) {
      DateTime now = DateTime.Now;
      year = ConvertDatePart(yearAsString, now.Year, 1990, 2099);
      month = ConvertDatePart(monthAsString, now.Month, 1, 12);
    }

    private void GetDateParts(string yearAsString, string monthAsString, string dayAsString, out int year, out int month, out int day) {
      DateTime now = DateTime.Now;
      year = ConvertDatePart(yearAsString, now.Year, 1990, 2099);
      month = ConvertDatePart(monthAsString, now.Month, 1, 12);
      day = ConvertDatePart(dayAsString, now.Day, 1, DateTime.DaysInMonth(year, month));
    }

The rest of the code is about validating and converting the strings coming from the views or URLs (i.e., the big, bad, outside world). If someone is trying to spoof the URLs, or the values are invalid, the current month or current date is assumed. I thought this was a better plan than merely throwing an exception.

In the final segment, I'll talk about how to create the archive folder (actually the archive post) and the view that you must create for it.

(Part 1 is here, part 2 here, part 3 here, part 4 here, part 4a here, part 4b here.)

Album cover for Hergest Ridge Now playing:
Oldfield, Mike - Part Two
(from Hergest Ridge)


Loading similar posts...   Loading links to posts on similar topics...

2 Responses

#1 Dew Drop - January 15, 2009 | Alvin Ashcraft's Morning Dew said...
15-Jan-09 8:32 AM

Pingback from Dew Drop - January 15, 2009 | Alvin Ashcraft's Morning Dew

#2 Writing an Archive Calendar, Part 3 said...
18-Jan-09 1:59 PM

Thank you for submitting this cool story - Trackback from DotNetShoutout

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response