This is part II 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.
Where Were We?
In Part I, I had gotten the basic engine working with some style capabilities (my own, not CSS) and the ability to click on a blog entry and go back to the home page. In this article:
- Limiting the blog entries retrieved from the database
- Adding an archive listing
- Adding categories
- Adding menus
Limiting the Blog Entries
For the normal listing, I want to limit the blog entries displayed to, say, 10 entries. Hmmm. Let's put this number into the database in the BlogInfo table. After adding the "MaxEntries" field to the database, I just add the property to the BlogInfo class:
protected long maxEntries;
public long MaxEntries
{
get { return maxEntries; }
set { maxEntries = value; }
}
and my poor man's ORM method will populate it properly. Now, how do I limit the number of rows retrieved from SQLite? Hmm. There's a "Limit" keyword for the select statement:
public static DataTable LoadBlogEntries(SQLiteConnection conn,
string id, long max)
{
SQLiteCommand cmd = conn.CreateCommand();
if (id == null)
{
cmd.CommandText = "select * from BlogEntry order by Date desc
limit "+max.ToString();
}
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;
}
Appears to work.
Archives
The Database Part
The archives should display a link for each month/year in which at least one blog entry has been made, and in parenthesis, the number of entries. The most difficult part about this is not the ASP.NET but rather figuring out an elegant query in SQLite. The query that appears to work for what I require is SELECT strftime('%Y-%m', date) as Date, count(strftime('%Y-%m', date)) as Num from blogentry group by strftime('%Y-%m', date) order by strftime('%Y-%m', date) desc
. Note that I want the listings in reverse chronological order. I created a helper class for the resulting rows:
public class ArchiveEntry
{
protected string date;
protected long num;
public long Num
{
get { return num; }
set { num = value; }
}
public string Date
{
get { return date; }
set { date = value; }
}
public ArchiveEntry()
{
}
}
The ASP.NET Part
I'm going to put the archives in the right gutter (yes, this is hard coded for now) and the categories in the left gutter. So, I'll need the right gutter cell to create the table that will list the archive entries, and this is done by just assigning cellArchive to the ID tag. I might as well assign cellCategory to the left cell as well while I'm at it. The problem that I see when testing it out is that the archive list is centered vertically on the page, rather than being aligned on the top like I want it to be. Setting the VerticalAlign property of the cell fixed that problem. The archives also need a to be links that provide the information for querying the blog entries for that month.
So now I have this method call for adding the archive table:
protected void LoadArchives()
{
SetStyle(cellArchive, "CellArchive");
DataTable dtArchives = Blog.LoadHistory(conn);
Table tblArchive = new Table();
SetStyle(tblArchive, "ArchiveTable");
cellArchive.Controls.Add(tblArchive);
foreach (DataRow row in dtArchives.Rows)
{
ArchiveEntry archive = new ArchiveEntry();
Blog.LoadProperties(archive, row);
TableRow rowArchive = new TableRow();
SetStyle(rowArchive, "ArchiveRow");
tblArchive.Rows.Add(rowArchive);
TableCell tblCellArchive = new TableCell();
SetStyle(tblCellArchive, "ArchiveEntry");
rowArchive.Cells.Add(tblCellArchive);
DateTime dt = Convert.ToDateTime(archive.Date);
string text = dt.ToString("MMMM yyyy") + " (" + archive.Num.ToString() +
")";
tblCellArchive.Text = "<a href=\"" + url + "?Year=" + dt.Year +
"&Month=" + dt.Month + "\">" + text + "</a>";
}
}
And the result, after adding some style information, looks like:
Not bad! And look, I'm blogging in the future! Now I just need to deal with the parameters when clicking on the link. We try getting the parameters:
...
string year = Request.QueryString["Year"];
string month = Request.QueryString["Month"];
string archiveDate = null;
if ((year != null) && (month != null))
{
archiveDate = year + "-" + month.PadLeft(2, '0');
}
and in the LoadBlogEntries query:
...
else if (archiveDate != null)
{
cmd.CommandText = "select * from BlogEntry where
strftime('%Y-%m', date)==@archiveDate order by Date desc";
cmd.Parameters.Add(new SQLiteParameter("archiveDate", archiveDate));
}
Categories
Did I say in Part I that a blog entry would have 1 or more categories? Wrong! A blog entry can have zero or one category. I'm being lazy and I'll want to work only with one category right now. Otherwise, I'd need another table to manage the one-to-many relationship between a blog entry and 0 or more categories.
The Database Part
The category table already exists, I just need my little helper class next. You'll note I added a Num property, because I want the categories to be like the archives--showing the number of posts in the category.
public class Category
{
protected long id;
protected string name;
protected string description;
protected long num;
.. etc ...
The query is select a.id as Id, a.name as Name, count(b.id) as Num from blogcategory a, blogentry b where a.id=b.categoryid group by a.name order by a.name
.
And the result (after creating some sample data) is:
You'll note I didn't bore you with the ASP.NET details, because they're basically identical to the archive listing. In fact, the code ought to be unified!
Menu Bar
I've saved what I consider will be the worst part for now. Along the principle of "keep it simple", I'll just create table cells and use my style setter to determine the look. But I'm loathe to hard code menus. I'd rather use the database to generate the menus and have them link to pages.
The Database Part
First, the helper class:
public class MenuItem
{
protected long id;
protected long menuBarId;
protected string text;
protected string page;
... etc ...
Then, some default menu bar stuff in the database's MenuBar table:
You'll notice I'm using a special blog entry for the Contact link rather than a separate page.
The ASP.NET Part
One thing I notice that's annoying is the menu items are spaced proportionally across the page. So if I have to menu items, the first in on the left and the second is in the middle of menu bar. I'd rather they be spaced evenly across the page. I tried the ASP.NET Menu class, and WTF!?!? It does the same thing!!! And gosh, I wonder why. When I look at the page source, it's using tables! AH! But the REAL reason is because I'm setting the width to 100%. I bet I was doing that in my original table as well. If I leave the width alone, it looks ok, except that the menu items are too close together. But I think I'll stick with the ASP.NET menu bar because it has a lot of interesting features. And the spacing between the menu items is set by the menu bar's StaticMenuItemStyle's HorizontalPadding property. That took a while to find. Of course, now any background color for the menubar only sets the area covered by the menu items! So I'm back to the original problem. My only solution to this is to create Panel object and put the menu bar inside the panel. The panel extends to the limits of the window, and then I can set both the panel and the menubar to the same background color.
Dynamically loading the menubar is easy enough:
protected void LoadMenuBar(int menuBarId)
{
menuBar.Items.Clear();
SetStyle(menuPanel, "MenuPanel");
SetStyle(menuBar, "MenuBar");
DataTable dtMenus = Blog.LoadMenuBar(conn, menuBarId);
MenuItem item=new MenuItem();
foreach (DataRow row in dtMenus.Rows)
{
Blog.LoadProperties(item, row);
menuBar.Items.Add(new System.Web.UI.WebControls.MenuItem(
item.Text, null, null, StringHelpers.LeftOfRightmostOf(url, '/') +
item.Page));
}
}
And after setting the Orientation property to horizontal, I get pretty much what I want:
Login And Administration
Time to deal with the login and basic administration, so I can move away from editing rows in the database by hand. For one thing, I need a username and password in the database! The default will be "Admin" for the username and password. The login page sets a session IsAdmin key if the login is valid and redirects to the admin page:
protected void btnLogin_Click(object sender, EventArgs e)
{
if ((tbUsername.Text == blogInfo.Username) &&
(tbPassword.Text == blogInfo.Password))
{
Session["IsAdmin"] = true;
Response.Redirect("admin.aspx");
}
}
Conversely, the logout page nulls the IsAdmin key and redirects to the home page:
protected void Page_Load(object sender, EventArgs e)
{
Session["IsAdmin"] = null;
Response.Redirect("default.aspx");
}
Because I'd like the website to have a consistent look and feel, the admin page uses the same header, menu, and footers:
And is set up essentially the same as the home page, except that a menu bar ID of 1 is specified. The test for whether we're in admin mode must be made for every admin page.
protected void Page_Load(object sender, EventArgs e)
{
if (Session["IsAdmin"] != null)
{
url = StringHelpers.LeftOf(Request.Url.ToString(), '?');
conn = (SQLiteConnection)Application["db"];
blogInfo = Blog.LoadInfo(conn);
Page.Title = blogInfo.Title;
Style.SetStyle(conn, tblHeader, "HeaderTable");
PageCommon.InitializeHeaderAndFooter(conn, url, blogInfo, cellTitle,
cellSubtitle, cellCopyright);
PageCommon.LoadMenuBar(conn, url, 1, menuBar, menuPanel);
}
else
{
Response.Redirect("login.aspx");
}
}
Because of this commonality, I've moved the initialization code into a static class, so the call for all the pages becomes:
PageCommon.LoadCommonElements(this, -1, out blogInfo,
out conn, out url, tblHeader,
cellTitle, cellSubtitle, cellCopyright,
menuBar, menuPanel);
which of course requires that the layout of each page contains the header, menu, and footer "pieces".
New Blog Entry
The new blog entry consists of a pulldown for the category, title and subtitle, and a multi-line textbox to paste in the HTML for the entry. As I said before, editing a blog entry online is klunky and I've found it to be buggy as well. I suppose it's nice when you're in an Internet Cafe, but frankly, that describes my blogging scenario 0.1% of the time. I'd rather use a nice HTML editor and simply copy and paste the HTML up to my blog. Why blog engines don't provide this simpler interface for this is beyond me. Instead, you get this rich text box editor that often takes half a minute to load, and then you have to click on the "HTML" button before posting the HTML. Sigh. Overly complicated, time consuming, and too many clicks.
The ASP.NET Part
Because I want HTML tags allowed in the entry, I have to explicitly turn off page validation:
You can disable request validation by setting validateRequest=false in the Page directive or in the configuration section. However, it is strongly recommended that your application explicitly check all inputs in this case.
Well, since only the admin will be making these posts, I don't see any problem with turning off validation. I still haven't figured out how to properly turn off validation just for the new blog entry page, so for the moment, this:
<system.web>
<pages validateRequest="false" />
is sitting in the Web.Config file. Of course, this turns off validation for ALL pages, which isn't what I want either.
Here's the code for entering a blog entry. Note the use of the BlogEntry helper class:
public partial class NewEntry : System.Web.UI.Page
{
protected BlogInfo blogInfo;
protected SQLiteConnection conn;
protected string url;
protected void Page_Load(object sender, EventArgs e)
{
if (Session["IsAdmin"] != null)
{
PageCommon.LoadCommonElements(this, 1, out blogInfo, out conn,
out url, tblHeader, cellTitle, cellSubtitle, cellCopyright,
menuBar, menuPanel);
if (!IsPostBack)
{
cbCategory.DataSource = Blog.LoadCategories(conn);
cbCategory.DataTextField = "Name";
cbCategory.DataValueField = "ID";
cbCategory.DataBind();
}
}
else
{
Response.Redirect("login.aspx");
}
}
protected void btnPost_Click(object sender, EventArgs e)
{
BlogEntry blogEntry = new BlogEntry();
blogEntry.CategoryId = Convert.ToInt64(cbCategory.SelectedValue);
blogEntry.Date = DateTime.Now;
blogEntry.Title = tbTitle.Text;
blogEntry.Subtitle = tbSubtitle.Text;
blogEntry.BlogText = tbBlogEntry.Text;
Blog.SavePost(conn, blogEntry);
}
}
The Database Part
There's not much to writing out the record. I use reflection to create the parameter list, which is over-simplified and probably not re-usable as it stands:
public static void SavePost(SQLiteConnection conn, BlogEntry blogEntry)
{
SQLiteCommand cmd = conn.CreateCommand();
cmd.CommandText = "insert into BlogEntry (Title, Subtitle, CategoryID,
Date, BlogText) values (@Title, @Subtitle, @CategoryID, @Date, @BlogText)";
AddParameters(cmd, blogEntry);
cmd.ExecuteNonQuery();
}
public static void AddParameters(SQLiteCommand cmd, object source)
{
foreach (PropertyInfo pi in source.GetType().GetProperties())
{
cmd.Parameters.Add(new SQLiteParameter(pi.Name,
pi.GetValue(source, null)));
}
}
Edit Categories
For editing categories, I'd like to try and use a built-in grid control. I must say, working with the built-in GridView is abysmal. In fact, I've spent three hours now trying to figure out why it doesn't update the underlying table row when I edit a value. The only thing I can figure out at this point is that I need to manually move the data into the underlying table, which is counter to all the examples I've seen. And then it seems that I have to use templates to get access to the data being edited in the cell. There's an example of dynamic templates fields here, but I simply cannot believe it is this difficult to work with a GridView control! Another article here, showing how this is done with the designer when you have the data source. Unbelievable. This completely entangled the presentation layer with the data layer, and I might as well learn how to do this dynamically (using G. Mohyuddin's article in the first link), as I absolutely refuse to hardcode my pages with information from the table's schema.
Finally though, I get things working. May not be the best way nor even the right way, but it works, and it works thanks to the example by G. Mohyuddin.
The ASP.NET Part
The most interesting part is where the templated fields are created. Because the fields are being dynamically created, a considerable amount of extra work is required.
protected void CreateTemplatedGridView()
{
gvCategories.Columns.Clear();
foreach (DataColumn dc in dtCategories.Columns)
{
DataControlField dcf = null;
if (dc.ColumnName.ToLower() == "id")
{
BoundField bf = new BoundField();
bf.ReadOnly = true;
bf.Visible = false;
dcf = bf;
}
else
{
TemplateField tf = new TemplateField();
tf.HeaderTemplate = new DynamicallyTemplatedGridViewHandler(
ListItemType.Header, dc.ColumnName, dc.DataType.ToString());
tf.ItemTemplate = new DynamicallyTemplatedGridViewHandler(
ListItemType.Item, dc.ColumnName, dc.DataType.ToString());
tf.EditItemTemplate = new DynamicallyTemplatedGridViewHandler(
ListItemType.EditItem, dc.ColumnName, dc.DataType.ToString());
dcf = tf;
}
gvCategories.Columns.Add(dcf);
}
}
Refer to the article here for a more complete description of what's going on. There's certainly no way I would ever have figured this out!
The RowUpdating event handler now can access the Text property of the template field, which is a TextBox. It loads the data into my Category helper class and passes that to the "data access layer", haha, to update the record.
void OnRowUpdating(object sender, GridViewUpdateEventArgs e)
{
GridViewRow row = gvCategories.Rows[e.RowIndex];
Category cat = new Category();
DataRow dataRow = dtCategories.Rows[e.RowIndex];
foreach (DataColumn dc in dtCategories.Columns)
{
TextBox tb = row.FindControl(dc.ColumnName) as TextBox;
if (tb != null)
{
string val = tb.Text;
dataRow[dc] = val;
}
}
Blog.LoadProperties(cat, dataRow);
Blog.UpdateCategory(conn, cat);
dtCategories.AcceptChanges();
gvCategories.EditIndex = -1;
BindData();
}
The Database Part
The UpdateCategory method uses my little reflection, poor man's ORM utility:
public static void UpdateCategory(SQLiteConnection conn, Category cat)
{
SQLiteCommand cmd = conn.CreateCommand();
cmd.CommandText = "update BlogCategory set mailto:Name=@Name">Name=@Name,
Description=@Description where ID=@ID";
AddParameters(cmd, cat);
cmd.ExecuteNonQuery();
}
The code is nearly identical for deleting and inserting a category; the SQL is different of course. One caveat here is that I haven't implemented cascading deletes, so deleting a category does not delete blog entries that reference that category.
Edit Style
This is why I hate ASP.NET development. Because the simplest solution to creating the EditStyle page, which, presentation and behavior-wise is identical to editing categories, is to copy and paste the friggin' code. Yuck. OK, code, take 2. I though I might use generics, but because of the GridView events that have to be hooked, generics seemed not really possible. The only issue with a code re-use approach is that the calls to the data access layer to update, insert, and delete is method-name specific. There are a variety of workarounds to this, and I chose to simply pass a string for the table being operated on (I did this for a certain amount of consistency, but it's still an awful implementation).
I did use generics in one place--the new record handler:
protected void btnNewCategory_Click(object sender, EventArgs e)
{
GridEdit.NewRecord<Category>();
}
I can now create the style editor (a simple grid, but good for now), and the entire Page_Load becomes:
protected void Page_Load(object sender, EventArgs e)
{
if (Session["IsAdmin"] != null)
{
PageCommon.LoadCommonElements(this, 1, out blogInfo, out conn, out url,
tblHeader, cellTitle, cellSubtitle, cellCopyright, menuBar, menuPanel);
Session["Conn"] = conn;
if (!IsPostBack)
{
dtStyles = Blog.LoadStyles(conn);
Session["Styles"] = dtStyles;
}
else
{
dtStyles = (DataTable)Session["Styles"];
}
GridEdit.PageLoad(gvStyles, dtStyles, "StyleRecord");
}
else
{
Response.Redirect("login.aspx");
}
}
which, in my opinion, is much more re-usable than copying and pasting 99.9% identical code from one page to the next, and the next, and so on.
Because my poor-man's ORM/reflection methods already work with the business objects as objects rather than hardcoded class types, the data access layer takes very little work to change over to the general purpose GridEdit helper. For example, the NewRecord (which used to be NewCategory) now looks like:
public static long NewRecord(SQLiteConnection conn, object rec)
{
SQLiteCommand cmd = conn.CreateCommand();
switch (rec.GetType().Name)
{
case "Category":
cmd.CommandText = "insert into BlogCategory (Name, Description)
values (@Name, @Description);select last_insert_rowid() AS [ID]";
break;
case "StyleREcord":
cmd.CommandText = "insert into Style (CellType, PropertyName,
FontPropertyName, Value) values (@CellType, @PropertyName,
@FontPropertyName, @Value);select last_insert_rowid() as [ID]";
break;
}
AddParameters(cmd, rec);
long id = Convert.ToInt64(cmd.ExecuteScalar());
return id;
}
Now this is at least a start as to how programming ASP.NET should be done when dealing with pages that are nearly identical in functionality and only different in the data that they manipulate!
Blog Setup UI
This is trivial now that I have the general purpose grid editor working. I'll use the same mechanism to edit the blog setup fields, except it won't have a button for adding a new record, since there's only one setup row for the blog. Also, deleting the record is disallowed. And 5 minutes later:
All done with that page.
Edit Blog Entries
Thinking out loud here...
Same with blog entries. It took about 5 minutes to create. Now, I'm going to be really lazy. The blog entry includes a category ID, which at the moment is displayed as an integer. It would be much better to of course display a pulldown list. I'll do that later. I can imagine it's going to take me another 3 hours to figure out how to do that with a template field. And I'm sure your saying "oh, this blows away Marc's re-use method for grids!" Not so fast. I'm planning on using attributes to decorate the property so that I can associate a pulldown with a property.
Another issue is that the blog entry is displayed in its entirety when not edited, in a huge TextBox. This will make the list unwieldy very quickly. And when you go to edit the blog entry, you get a small little textbox. I can live with that, as I'll copy the original entry to an HTML editor and paste in the changes.
And when I think about it, instead of displaying a grid, it would simply be easier to edit the blog entry by being in admin mode and selecting the blog. I like that idea much better! Then I don't need to deal with attributes, huge text boxes, pagination, etc. Woohoo! Design a web app on the fly! Don't do this at home, folks!
OK, so to support this feature I added a hidden ID and date field and added the logic for the button visibility based on whether the user is in admin mode and editing the entry. The update and delete methods are quite simple. Notice the redirect to the home page when the entry is deleted and the redirect with the entry ID when the entry is updated.
protected void btnUpdate_Click(object sender, EventArgs e)
{
BlogEntry blogEntry = new BlogEntry();
blogEntry.Id = Convert.ToInt64(tbID.Text);
blogEntry.CategoryId = Convert.ToInt64(cbCategory.SelectedValue);
blogEntry.Date = Convert.ToDateTime(tbDate.Text);
blogEntry.Title = tbTitle.Text;
blogEntry.Subtitle = tbSubtitle.Text;
blogEntry.BlogText = tbBlogEntry.Text;
Blog.UpdateRecord(conn, blogEntry);
Response.Redirect("default.aspx?ID="+blogEntry.Id.ToString());
}
protected void btnDelete_Click(object sender, EventArgs e)
{
BlogEntry blogEntry = new BlogEntry();
Blog.DeleteRecord(conn, "BlogEntry", Convert.ToInt64(tbID.Text));
Response.Redirect("default.aspx");
}
Page Title
Hmm. I just noticed the page title, as it appears in a Firefox tab, is "untitled page". Yuck! One line in the Page_Load method fixes that!
Page.Title = blogInfo.Title;
RSS
OK, this is the next big issue to tackle. Getting an RSS link working so my blog works with aggregators. Wikipedia has some information on the RSS format and I guess I'll try RSS 2.0 format. This link provides a nice summary of required and optional elements. There is nothing thrilling about the code that generates the RSS--it's ugly and brute force. I haven't supported all the optional RSS tags. The RSS is updated whenever a blog post is made or a blog post is changed. The nice thing is that the DateTime.ToString() method has a format specifier "r" that formats the string into the correct format for the RSS!
I've added the RSS link to the archives columns, as this seems to be a reasonable place for it to go. I tested the RSS with FeedReader and it worked fine. Testing with Google's reader, the link is still stuck on a different header and content. Google must be caching the link and not updating it very frequently. Or I'm not supplying some information it's expecting. Wierd.
Things I've Learned
Learning ASP.NET reminds me somewhat of learning to write games for a Commodore PET. It seems klunky and I'm not sure I'm doing things in the right way. Far too much time is spent fussing with the presentation layer. ASP.NET development is a ripe platform for all kinds of automation to help smooth out the rough edges with presentation, usability, and data management. There seems to be no clean separation of UI layer, business layer, and data layers. Any separation one adds doesn't really feel right because the presentation layer and its complexities is always in your face.
I'm sure I've made a lot of mistakes putting together this web app, but it's taught me a few things and I imagine, if I were to do it again, I would look more closely at third party tools to make the presentation layer easier to work with, some real automation tools for tying the presentation layer and business objects together, and better separation of concerns between the business layer and data layer. Also something that needs a serious looking at is the repetition and redundancy that occurs from page to page. In many ways, web pages are like little autonomous islands. Working with them so that there is something common and consistent between them is very difficult.
And because everything is stateless, I find that I end up writing a lot of static classes to promote re-usability, and passing objects in to those classes. So there is little that strikes me as object oriented regarding ASP.NET development. Certainly, the ASP.NET development environment is very slick. The ability to debug applications, switch between design and HTML code, the automatically generated Javascript, etc., make it very easy to put together reasonable looking, functional, websites quickly.
The GridView is a hideous control. Obviously, a lot of thought has gone into making it as customizable as possible. This does not deter from the fact that it is plain ugly. The way the presentation changes when going into edit mode is unacceptable, in my opinion. The visual presentation of the control and its usability is so poor, I can see why people scramble for third party packages that offer some hope of a better presentation than what comes out of the box with ASP.NET.
While elements on web pages are supposed to "flow", I find that in practice the flow of a web elements is problematic and fraught with problems. Sometimes web elements start on a "new line" for no explainable reason, and setting an element's visibility has inconsistent behavior--horizontally, it appears that other elements collapse but vertically the space occupied by an invisible element remains. There seems to be little rhyme or reason (or control) to flow. As a result, basic web layout looks ugly because of alignment problems.
One thing that is driving me nuts is the purple "You've already clicked on this link" colorization. It makes the whole website look like it has the measles after clicking around a bit. However, this is something I'll deal with later.
In this project I had to work with:
- ASP.NET (hardly know it)
- IIS configuration (lots of googling to get answers to problems)
- CSS styles (no knowledge at all, had to ask Mark Harris)
- database stuff (SQLite has its nuances, but at least I'm an experienced DB person, which you wouldn't notice I suppose given that I didn't put any FK's into the database, etc).
- C# (I consider myself an expert, but web app development is a different beast and impacts design issues that I'm only scratching the surface of)
Things/issues a web app often deals with that I didn't:
- Graphics (I am no graphic artist)
- Javascript (say what?)
- AJAX (sigh)
- Performance (load balancing, db performance, volume, etc.)
- CSS for skins/themes (know nothing about)
- Security (very important! Which I've essentially ignored)
And this is just for ASP.NET. I really admire people who know ASP.NET and PHP, Apache webserver, Linux, and things like Ruby. How do they have time to actually get anything done?
Installation
Copy the files to your webserver. You will also need to install SQLite ADO.NET (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 database blog.db and the rss.xml file must be set up for write permissions. 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:
Conclusion
(A conversation with Mark Harris)
Mark: so with this, how does one change the layout of the blog?
Marc: you can't. Not in the "requirements" :)
Mark: lol k
Mark: I hope you used MasterPages so people can purdy it up easy
Marc: Pushkin wants to know what "master pages" are. (I figure, if the cat asks, I won't look like an ignoramus)
I guess there might be a Part III one day: "Refactoring The Blog Engine".
So let's see how I compare against my original rant about other people's blog engines:
- Resolved: 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: None known (famous last words)
- they're overly complicated (mine seems simple to me, but I've been accused of needing to dumb down what I thought was simple code)
- they make assumptions about presentation (I totally failed that!)
- are nearly impossible to customize (no CSS, but not the easiest thing to customize either!)
- require a database I don't want to add to my server (ok, succeeded there. SQLite is nice and portable).
Not great. But it was fun adventure!
Tools
SQLite ADO.NET
SQLite Query Analyzer