This is Part I of a two part series.
Introduction
First off, you can view the blog live here.
The following is an article "with attitude" on putting together a basic blog engine with .NET 2.0.
Why am I writing a blog engine? Because I've never found one that I like. They either:
- require a platform or provider I don't want to learn or support (I run a dedicated server with W2003 Web Edition and IIS)
- they're buggy
- they're overly complicated
- they make assumptions about presentation
- are nearly impossible to customize
- require a database I don't want to add to my server
And because I'm a maverick and I like to do my own thing. I've tried DotText, and it's clunky and obsolete. I've tried SubText, and I can't figure out how to change simple things like the widths of the margins. And the other blog engines? Forget it. I can't even make my way around their websites to figure out what the requirements are or how to install them.
And amazingly, there is not one article on CodeProject regarding a blog engine. So here's the first one (assuming one doesn't get posted in the next day or so).
Let The Buyer Beware
I will probably say this a few times in this article, but I think this is the first time: I'm rather clueless when it comes to web apps. So I'm doing this to learn how not to write a web app, as I'm sure I'll learn a lot more of that than "how to". So this is also a foray into the wonderful world of web applications and the things I discover while writing what should be a simple application. You should be aware, if you use this code (I can't imagine anyone would actually want to use it) that this is most likely the wrong way to do things.
Requirements
Use a Lightweight Database
A blog engine should not require a heavy duty database like SQL Server or MySQL or any other flavor. It should practically be doable in XML! However, a database does have some advantages, and SQLite is a nice, easy to use package. So the database engine will be SQLite.
Do Blogging, And Nothing But Blogging
Except for RSS. I basically want an engine where I can publish a post. That's it. How complicated can that be? And for the life of me, I can't understand why anyone would want to use a clunky browser-based text/HTML editor rather than a nice HTML editor from the comfort of one's home, that's capable of spell checking, getting the formatting right, etc. Using a thin client is a great idea but unless you're Google, I'll stick with my desktop tools for comfortable editing of my blogs, thank you.
RSS
Yes, we need RSS, so people can use their aggregators and be notified of updates.
Blogs, Blog Entries and Categories
The Blog Itself
Consists of:
- A header, including the blog title and subtitle and menu
- The body, consisting of blog entries
- A footer, consisting of whatever, such as copyright information
- Optional left and right margins for archives and categories
- An RSS link
The archives should be a list of entries per month, excluding months where no entry was made.
The categories should be a list of category names.
The menu, frankly, I'm not thrilled with. What do I need a menu for, except to put the blog into admin mode so I can delete or update and entry, modify the UI, or add a new entry. Oh well. If I'm going to have a menu, it might as well be customizable to some degree. More on this later.
Blog Entry
A blog entry should have:
- a title
- a subtitle/summary
- an optional category
- date/time published
- the blog text itself
A category should have:
- The category name
- A description
Keep It Simple
The code should be simple. It should be elegant. It should be designed. It should be commented. I look at other blog engines and I go, WTF? The SubText source is 18.5 MB!
I'm not an CSS wizard nor an HTML wizard, and I suspect many other people aren't either. So my blog engine needs to embody the idea of "simple configurability". OK, the folks that know CSS and HTML will complain about nonstandard this or that. I don't really care. It needs to work for me, not them.
It's easy to get caught up in the whole customizability thing. Fonts, colors, justification, positioning, all that. Customization can wait, and the focus is, "lean and mean".
What I Don't Want
Comments
People to leave comments. You want to say something, then email me. I haven't seen a blog engine yet that is impervious to spammers, except by completely disabling comments. Besides, I don't want to read your comments, I want you to read my blog.
Skins
Argh! I don't know CSS, I don't want to know CSS. I want the thing to look decent on its own. OK, I'll admit, people want their blog to express their individuality. That can wait. Given that I know next to nothing about web pages, learning to make something CSS'able isn't in the game plan right now.
VB
I don't want no stinkin' VB code. Enough said.
UI Design
Here's a diagram of the UI elements:
UI Implementation
Initial Design, Take 1
So after making sure that IIS is installed and that I re-install .NET 2.0 to work with IIS, I fire up VS2005 and start a new website project. I open up the default.aspx page in the designer and mumble about what to do next. Well, let's start simple. A panel for the header and footer, and table for the body. Add a few labels, and discover that labels can't be justified. Ah yes, I love HTML. So, wrap the label in a table! I suppose that makes more sense anyways, since a table can have multiple rows. And this means that we can get rid of the panels as well.
Here's a screenshot of the basic idea:
The gutters are at 20% width, the body at 60% width, and the header and footer are at 100% width.
As for the blog entry, it needs to be a table as well. Of course, I can't put a table as a cell in the designer! Oh no! The tables aren't HTML tables, they're ASP.NET tables!!!
<asp:TableCell...>Left Gutter</asp:TableCell>
<asp:TableCell ...>Blog Entry</asp:TableCell>
<asp:TableCell ...>Right Gutter</asp:TableCell>
Argh! That's not really what I wanted! OK, TableCell is a WebControl, what if I make a specialized TableCell that has a Table as a child control? That does NOT work (or at least, I couldn't get it to work). Instead, let's add the table programmatically when the page is loaded. And when you think about it, what we really have is:
- the blog is a collection of blog entry rows
- a bog entry row consists of another table with rows for the title, subtitle, category, date, and blog text.
Initial Design, Take 2
So, we need to add some code to the Page_Load method. But first, I needed to assign an ID (which I called "cellBogEntry") to the middle cell of the blog entry table, so we can add the Table to its Controls collection.
protected void Page_Load(object sender, EventArgs e)
{
Table blogTable = CreateTable(new string[] { "Blog Entry" });
Table blogEntry = CreateTable(new string[] {"Title", "Subtitle",
"Category", "Date", "Blog Text"});
blogTable.Rows[0].Cells[0].Controls.Add(blogEntry);
cellBlogEntry.Controls.Add(blogTable);
}
and define the CreateTable method:
protected Table CreateTable(string[] rowNames)
{
Table table = new Table();
table.Width = Unit.Percentage(100);
table.BorderStyle = BorderStyle.Solid;
table.BorderWidth = 1;
foreach (string rowName in rowNames)
{
TableRow tr = new TableRow();
TableCell tc = new TableCell();
tc.Width = Unit.Percentage(100);
tr.Cells.Add(tc);
tc.Text = rowName;
table.Rows.Add(tr);
}
return table;
}
and the result is:
Good! That looks more like what I have in mind.
Now let's take a break from the UI and get the database going.
Database Design
We need some tables to get us started:
BlogInfo
Consists of the fields:
- Title
- Subtitle
- Email
- Copyright
BlogCategory
BlogEntry
- Title
- Subtitle
- CategoryID
- Date
- BlogText
Database Implementation
Since I'm planning to use SQLite, I want to make certain considerations for dealing with a stateless runtime environment. First, I don't want to be opening and closing connections to the database constantly. I might as well have one connection and leave it open.
Application State
The database instance will be stored in the application state so that each application instance can use the same database instance. Frankly, in reading the documentation, I'm confused by application state and session state. I would have thought that application state is permanent, but what appears to be happening is that each client creates an instance of the application. But then what is a session? Regardless, this is the key piece of information that I'm looking for:
The Application_Start and Application_End methods are special methods that do not represent HttpApplication events. ASP.NET calls them once for the lifetime of the application domain, not for each HttpApplication instance.
which is what I want to create the database instance and initialize it if necessary. I learn that this is done in the global.asax file. Hmm. There isn't one. Ah, but there's a "Global Application Class" template that can be added to the project. After adding the file, I also add a reference to System.Data.SQLite. Referenced assemblies, I discover, get added to the web.config file (I know nothing about web development, sigh). Working with the global.asax file is very odd to me, as classes need to be fully qualified since I can't figure out where to put a "using" statement.
Website Paths
The question is, where did it put the blog.db file!?!?! Well, if I add a watch for System.IO.Path.GetFullPath("blog.db"), I discover that the file has been put in c:\Program Files\Microsoft Visual Studio 8\Common7\IDE\blog.db! Oh my, that's not really what I want. There should be some saner place than that, something associated more with the website itself. Why didn't the file get created in the "App_Data" folder? Reading about website paths, MSDN says:
You can use the ~ operator in any path-related property in server controls.
but bool dbExists = System.IO.File.Exists("~/App_Data/blog.db");
doesn't work. But an alternative (probably a wrong one) exists, as shown in the code:
void Application_Start(object sender, EventArgs e)
{
string dbPath = Server.MapPath("~") + "\\App_Data\\blog.db";
bool dbExists = System.IO.File.Exists(dbPath);
System.Data.SQLite.SQLiteConnection conn =
new System.Data.SQLite.SQLiteConnection();
conn.ConnectionString = "Data Source="+dbPath;
conn.Open();
if (!dbExists)
{
CreateDatabase(conn);
}
Application.Add("db", conn);
}
void CreateDatabase(System.Data.SQLite.SQLiteConnection conn)
{
using (System.Data.SQLite.SQLiteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = "create table BlogInfo (Title text, Subtitle text,
Email text, Copyright text)";
cmd.ExecuteNonQuery();
cmd.CommandText = "create table BlogCategory (ID integer primary key
autoincrement, Name text, Description text)";
cmd.ExecuteNonQuery();
cmd.CommandText = "create table BlogEntry (ID integer primary key
autoincrement, Title text, Subtitle text, CategoryID integer,
Date datetime, BlogText text)";
cmd.ExecuteNonQuery();
cmd.CommandText = "insert into BlogInfo (Title, Subtitle, Email,
Copyright) values ('Title', 'Subtitle', 'your email',
'your copyright')";
cmd.ExecuteNonQuery();
}
}
void Application_End(object sender, EventArgs e)
{
System.Data.SQLite.SQLiteConnection conn =
(System.Data.SQLite.SQLiteConnection)Application["db"];
conn.Close();
}
Loading The Page With Data
The Header And Footer
So let's go back to the UI now and get some data showing up in the blog. You'll note I created some initial data for the BlogInfo table, since this is a single-row table. So, going back to the designer, I've added cellTitle, cellSubtitle, and cellCopyright ID's to the cells in the header table. I also used the useful SQLite Query Analyzer to modify the BlogInfo data.
So my next problem is that I want to add some helper classes, and I figure I don't need to add these to the App_Code folder (Visual Studio asks). Well, I was wrong. They need to be. I have no clue why.
The code for initializing the header is straight forward. I get the connection object and populate the TableCell text. BlogInfo is just a collection of properties with getters and setters.
protected void InitializeHeader()
{
SQLiteConnection conn = (SQLiteConnection)Application["db"];
BlogInfo blogInfo = Blog.LoadInfo(conn);
cellTitle.Text = blogInfo.Title;
cellSubtitle.Text = blogInfo.Subtitle;
cellCopyright.Text = blogInfo.Copyright;
}
LoadInfo reads the one and only row:
public static BlogInfo LoadInfo(SQLiteConnection conn)
{
SQLiteCommand cmd = conn.CreateCommand();
cmd.CommandText = "select * from BlogInfo";
SQLiteDataAdapter da = new SQLiteDataAdapter(cmd);
DataTable dt = new DataTable();
da.Fill(dt);
BlogInfo blogInfo = new BlogInfo();
LoadProperties(blogInfo, dt.Rows[0]);
return blogInfo;
}
and since I'll be using helper classes like BlogInfo for the other tables, I'll implement a poor-man's ORM using reflection to move data between the helper class and the DataRow:
public static void LoadProperties(object target, DataRow row)
{
foreach (PropertyInfo pi in target.GetType().GetProperties())
{
if (row.Table.Columns.Contains(pi.Name))
{
pi.SetValue(target, row[pi.Name], null);
}
}
}
And the result is slowly coming alive:
Some Sample Blog Entries
Now let's add a couple sample blog entries and see how they display. I'll use the blogEntry Table that was created earlier (but I needed to move it so that it is available to class for this request). The next problem I run into has nothing to do with ASP.NET. The DateTime in the SQLite isn't being converted to something .NET understands. After about 20 minutes of digging, I discover that it's probably because I didn't enter the date/time information correctly when writing the inserts, as per this notation. If I use "datetime('now')" to insert my test data, I don't get a the string conversion exception. Unfortunately, I get a completely bogus date of 1/1/0001 12:00:00 AM! And among other problems, the ID's need to be long not int, since they're int64's in SQLite, and the LoadProperties method needs to handle DBNull.Value values, as .NET is too stupid to convert this to a null for a nullable value type. Argh!
public static void LoadProperties(object target, DataRow row)
{
foreach (PropertyInfo pi in target.GetType().GetProperties())
{
if (row.Table.Columns.Contains(pi.Name))
{
object value = row[pi.Name];
if (value == DBNull.Value)
{
value = null;
}
pi.SetValue(target, value, null);
}
}
}
And as it turns out, the bogus date is my fault--the property name in the BlogEntry helper class didn't match the table column name.
And here we are:
Notice the entries are sorted in reverse order. The code is a bit ugly. First, populating the ASP.NET rows and cells (more snazzy reflection):
protected void LoadBlogEntries()
{
SQLiteConnection conn = (SQLiteConnection)Application["db"];
DataTable dt = Blog.LoadBlogEntries(conn);
blogTable.Rows.Clear();
BlogEntry blogEntry=new BlogEntry();
foreach (DataRow row in dt.Rows)
{
Table blogEntryTable=new Table();
AddEntryToTable(blogEntryTable);
Blog.LoadProperties(blogEntry, row);
foreach (string propName in new String[] { "Title", "Subtitle", "Date",
"BlogText" })
{
TableRow tr = new TableRow();
blogEntryTable.Rows.Add(tr);
TableCell tc = new TableCell();
tr.Cells.Add(tc);
PropertyInfo pi = blogEntry.GetType().GetProperty(propName);
tc.Text = pi.GetValue(blogEntry, null).ToString();
}
}
}
protected void AddEntryToTable(Table blogEntryTable)
{
TableRow tr = new TableRow();
blogTable.Rows.Add(tr);
TableCell tc = new TableCell();
tr.Cells.Add(tc);
tc.Width = Unit.Percentage(100);
tc.Controls.Add(blogEntryTable);
}
And also, getting the data from the database:
public static DataTable LoadBlogEntries(SQLiteConnection conn)
{
SQLiteCommand cmd = conn.CreateCommand();
cmd.CommandText = "select * from BlogEntry order by Date desc";
SQLiteDataAdapter da = new SQLiteDataAdapter(cmd);
DataTable dt = new DataTable();
da.Fill(dt);
return dt;
}
Making It Prettier
OK, let's ignore menus and categories and archives and administration, and let's make what we have a bit nicer looking. And let's forget CSS and style sheets and all that as well. I really shouldn't, of course, since it's the "right" way of doing things. And I won't even try to figure out how theming works! Moving right along then...
I'd like to be able to set the:
- font name
- font style
- size
- alignment
- background color
- foreground color
for each type of cell. So, what types of cells to we have so far:
- header table
- header title
- header subtitle
- blog entry table
- blog entry title
- blog entry subtitle
- blog entry date
- blog entry
- copyright
So, let's use the database to store the style information that we want to manage, along with some information to work with these properties using reflection, since I hate hard coding these things. I'm going to add the table Style with the following fields:
- CellType
- PropertyName
- FontPropertyName
- Value
which, among other things, will be general enough to also specify the border style. So, to begin with, here's some SQL to set some styles:
insert into style (CellType, PropertyName, FontPropertyName, Value)
values ('HeaderTitle', null, 'Bold', 'true');
insert into style (CellType, PropertyName, FontPropertyName, Value)
values ('HeaderTitle', 'BackColor', null, 'LightBlue');
insert into style (CellType, PropertyName, FontPropertyName, Value)
values ('HeaderTitle', null, 'Size', '20');
insert into style (CellType, PropertyName, FontPropertyName, Value)
values ('HeaderTitle', null, 'Name', 'verdana');
Now, I quickly realized that "CellType" isn't really the right field name. "ObjectType" is much more accurate, as we can change table, row, and cell properties. So that'll have to be refactored. But the code is straight forward. Any object can be passed to the SetStyle method, which adjusts either a property of the object or a property of the object's Font property (assuming it has one):
protected void SetStyle(object obj, string key)
{
SQLiteConnection conn = (SQLiteConnection)Application["db"];
DataTable styles=Blog.GetStyles(conn, key);
foreach (DataRow style in styles.Rows)
{
if (style["PropertyName"] != DBNull.Value)
{
PropertyInfo pi = obj.GetType().
GetProperty(style["PropertyName"].ToString());
object val = style["Value"];
object newVal = Converter.Convert(val, pi.PropertyType);
pi.SetValue(obj, newVal, null);
}
else if (style["FontPropertyName"] != DBNull.Value)
{
PropertyInfo piFont = obj.GetType().GetProperty("Font");
FontInfo fontInfo = (FontInfo)piFont.GetValue(obj, null);
PropertyInfo pi = fontInfo.GetType().GetProperty(
style["FontPropertyName"].ToString());
object val = style["Value"];
object newVal = Converter.Convert(val, pi.PropertyType);
pi.SetValue(fontInfo, newVal, null);
}
}
}
and the DB query is:
public static DataTable GetStyles(SQLiteConnection conn, string key)
{
SQLiteCommand cmd = conn.CreateCommand();
cmd.CommandText = "select * from Style where CellType=@key";
cmd.Parameters.Add(new SQLiteParameter("key", key));
SQLiteDataAdapter da = new SQLiteDataAdapter(cmd);
DataTable dt = new DataTable();
da.Fill(dt);
return dt;
}
So, revisiting something like initializing the header, note the additional SetStyle calls:
protected void InitializeHeader()
{
SQLiteConnection conn = (SQLiteConnection)Application["db"];
BlogInfo blogInfo = Blog.LoadInfo(conn);
cellTitle.Text = blogInfo.Title;
SetStyle(cellTitle, "HeaderTitle");
cellSubtitle.Text = blogInfo.Subtitle;
SetStyle(cellSubtitle, "HeaderSubtitle");
cellCopyright.Text = blogInfo.Copyright;
SetStyle(cellCopyright, "Copyright");
}
and after a bunch of style entries in the database, we get:
So, I think it's coming along quite nicely! (The borders I can get rid of with my style feature, but I'm going to leave them in for now).
Blog Links
Can you tell I'm not good at writing requirements? A blog should let you click on an entry and view just that entry, and the URL should be usable as a reference so you can link to the entry directly from somewhere else. Let's figure out how that works. First, the blog entry title needs to be a link, so I'll put "a href" tags around the text and see how that goes. In my test case, that worked, but two questions exist:
- how do I get the URL for the site rather than having some entry that hardcodes it?
- how do I respond to an "?ID=1" (for example) query string?
Ah ha! Both are answered by two simple lines in the Page_Load method. One caveat, the resulting URL must be stripped of any query strings!
protected void Page_Load(object sender, EventArgs e)
{
id=Request.QueryString["ID"];
url = StringHelpers.LeftOf(Request.Url.ToString(), '?');
...
And I modified the Blog.LoadBlogEntries to use an ID as a qualifier if one exists.
public static DataTable LoadBlogEntries(SQLiteConnection conn, string id)
{
SQLiteCommand cmd = conn.CreateCommand();
if (id == null)
{
cmd.CommandText = "select * from BlogEntry order by Date desc";
}
else
{
cmd.CommandText = "select * from BlogEntry where ID=@id";
cmd.Parameters.Add(new SQLiteParameter("id", id));
}
SQLiteDataAdapter da = new SQLiteDataAdapter(cmd);
DataTable dt = new DataTable();
da.Fill(dt);
return dt;
}
Success!
But now we'll need the blog title to be clickable back to the home page. Ahhhh! Feature creep! Ahhh! Poor initial requirements!
cellTitle.Text = "<a href=\"" + url+"\">" + blogInfo.Title + "</a>";
And what's with the underlining? It'd be nice if the titles weren't underlined. To do that, you won't believe this, but we need CSS (thank you Mark Harris)! At the beginning of each Page_Load:
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("<style>A {text-decoration: none}</style>");
...
Now the underlying is gone! Mark Harris says: "you shouldn't be writing html out from code.. that's just evil!" Hehe. I suppose I should just embed the style info into the page's HTML.
What's Next
- OK, this article is getting long enough. Tell me what I'm doing wrong and what you like and don't like, and I may do something about what you don't like, but probably not. I'll tell you what I don't like though--a variety of hard coded constants, functionality, and the inability to define the "flow" of the various sections of the page.
- In the next installment, I'm going to deal with menus, categories, and archives. At this point though, you'll note that I'm not really dealing with ASP.NET anymore, but rather with the drudgery of getting information out of the database and presenting it on the page, and non-existent error checking.
- I've also traded off the complexity of CSS for the complexity of editing a database table! That's not cool, and so I'm going to have to add some nice admin features, because frankly, I don't want the user (or me) to have to touch the database.
- And the final admin features include adding, updating and deleting a post.
- The blog entries need to be qualified by some limit.
- Oh, and let's not forget about RSS!
Aaaah! I feel feature creep happening!
When I get all done, I hope I'll have a lean and mean blog engine that should take up less than 100K of source code. Of course, then I have to start using it!
Things I've Learned
- Writing a basic ASP.NET application is fairly easy
- Most of the work is in presentation, data gathering, and administration
- The devil is really in the details of customizability and usability
- To delete the database, I have to kill the "WebDev.WebServer.EXE" process that has a file lock on the blog.db file.
Installation
Copy the files to your webserver. You will also need to install SQLite (see link below). Adjust the web.config file accordingly.
Since this is an ASP.NET 2.0 application, I learned about dealing with running IIS with both ASP.NET 1.1 and 2.0. Read more here about application pools, as you'll need to create a separate application pool for a ASP.NET 2.0 application, if you're running both 1.1 and 2.0 together.
The directory that the database is in must also be set up for write access. I had to change the IIS_WPG account for write permissions for the App_Data folder and the root folder to enable writing of the database and rss.xml file respectively. That's probably totally the wrong thing to do.
If you delete the database, the application will create the database with some initial values. You'll need to create records in the style list to make it pretty, otherwise it looks like this:
Tools
SQLite ADO.NET
SQLite Query Analyzer