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}&month={2}", GetArchivePath(), year, month); } private string GetArchivePathForDay(int day) { return string.Format("{0}?year={1}&month={2}&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 &
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 ? "« {0}" : "{0} »"; 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.)
Now playing:
Oldfield, Mike - Part Two
(from Hergest Ridge)
2 Responses
#1 Dew Drop - January 15, 2009 | Alvin Ashcraft's Morning Dew said...
15-Jan-09 8:32 AMPingback from Dew Drop - January 15, 2009 | Alvin Ashcraft's Morning Dew
#2 Writing an Archive Calendar, Part 3 said...
18-Jan-09 1:59 PMThank 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.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response