When I decided to start adding technical articles to this site I looked
for blogging software or a content management system that I could
integrate fairly easily with my existing pages. That meant it had to run
under IIS and use ASP.NET. While I found some really nice software, most
of them were either intended as a stand alone product or used newer
technologies that didn't easily integrate into my existing web site. I
plan to incorporate one of those products into a future rewrite using
Blazor, but for now I chose to build a rudimentary system that would meet
my current needs without a lot of work.
I started out by defining an Article class that included the fields I
thought I would need such as the article body and some metadata about the
article. The class ended up looking like this:
using System;
using System.Collections.Generic;
using System.Web;
namespace MyWebSite.Classes
{
public class Article : IComparable<Article>
{
public int Id { get; set; } = 0;
public string Title { get; set; } = "Article not found";
public string Author { get; set; } = "Me";
public DateTime Date { get; set; } = DateTime.Now;
public string Category { get; set; } = "Radio";
public string Summary { get; set; } = "This article could not be found";
public string Content { get; set; } = "";
public int CompareTo(Article other)
{
if (other == null)
return 1;
else
return this.Date.CompareTo(other.Date);
}
}
}
Now that I had the article definition, I needed a way to load and store
the articles. For simplicity I used an XML file in the App_Data
folder of my web site. You could certainly choose to use a database,
JSON, or whatever format you like. This just seemed like a simple
solution. For now I only defined two methods. The first
returns a list of articles. The second returns a specific article
based on the Id.
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Xml.Serialization;
namespace MyWebSite.Classes
{
public class ArticleRepository
{
public List<Article> GetArticles()
{
string folderPath = AppDomain.CurrentDomain.BaseDirectory;
string blogFilePath = Path.Combine(folderPath, "App_Data", "articles.xml");
using (var stream = System.IO.File.OpenRead(blogFilePath))
{
var serializer = new XmlSerializer(typeof(List<Article>));
List<Article> list = serializer.Deserialize(stream) as List<Article>;
list.Sort();
list.Reverse();
return list;
}
}
public Article FindArticle(int id)
{
List<Article> posts = GetArticles();
foreach(Article post in posts)
{
if (post.Id == id)
{
return post;
}
}
return null;
}
}
}
Now that I had my article repository ready I could create some web pages.
The first page I created was a page to display the list of articles as
links. Clicking on a link would open up a details page with the full
article. Later I decided to add some basic filtering by category, author,
or month. The list page is pretty simple. It retrieves the list of
articles and builds the links for display on the page.
Here's the page:
<%@ Page Async="true" Title="Tech Articles" Language="C#" MasterPageFile="~/Site.Master" MetaKeywords="Technical Articles" MetaDescription="A list of technical articles/blog postings" AutoEventWireup="true" CodeBehind="ArticleList.aspx.cs" Inherits="MyWebSite.ArticleList" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div class="jumbotron shadow">
<div class="text-center">
<h1>Technical Articles</h1>
<p class="lead">Articles about radio, programming, and creating web sites</p>
</div>
</div>
<main role="main" class="container">
<div class="row">
<div class="col-md-8 blog-main">
<h3 class="pb-4 mb-4 font-italic border-bottom">Article List</h3>
<% foreach (var post in Articles)
{ %>
<div class="blog-post">
<h3 class="blog-post-title"><%= post.Title %></h3>
<a href="/ArticleDetails?id=<%=post.Id%>"><%=post.Summary%></a>
<p><small class="blog-post-meta">Author: <%=post.Author %> Date: <%=post.Date.ToString("MM/dd/yy") %> Category: <%=post.Category %></small></p>
</div>
<% } %>
</div>
<aside class="col-md-4 blog-sidebar">
<div class="p-4 mb-3 bg-light rounded">
<h4 class="font-italic">Categories</h4>
<asp:DropDownList ID="DropDownListCategories" runat="server" AutoPostBack="true" OnSelectedIndexChanged="DropDownListCategories_SelectedIndexChanged"></asp:DropDownList>
</div>
<div class="p-4 mb-3 bg-light rounded">
<h4 class="font-italic">Authors</h4>
<asp:DropDownList ID="DropDownListAuthors" runat="server" AutoPostBack="true" OnSelectedIndexChanged="DropDownListAuthors_SelectedIndexChanged"></asp:DropDownList>
</div>
<div class="p-4 mb-3 bg-light rounded">
<h4 class="font-italic">Archives</h4>
<asp:DropDownList ID="DropDownListArchives" runat="server" AutoPostBack="true" OnSelectedIndexChanged="DropDownListArchives_SelectedIndexChanged"></asp:DropDownList>
</div>
</aside>
</div>
</main>
</asp:Content>
And the code behind:
using MyWebSite.Classes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace MyWebSite
{
public partial class ArticleList : System.Web.UI.Page
{
public List<Article> Articles;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
LoadArticles();
RegisterAsyncTask(new PageAsyncTask(LoadDropDownLists));
}
}
private void LoadArticles()
{
ArticleRepository repo = new ArticleRepository();
Articles = repo.GetArticles();
}
private async Task LoadDropDownLists()
{
List<string> categories = new List<string>();
List<string> authors = new List<string>();
List<string> archives = new List<string>();
await Task.Run(() =>
{
// find each unique values in the articles list
foreach (Article art in Articles)
{
if (!categories.Contains(art.Category))
{
categories.Add(art.Category);
}
if (!authors.Contains(art.Author))
{
authors.Add(art.Author);
}
string monthYear = art.Date.ToString("MMMM yyyy");
if (!archives.Contains(monthYear))
{
archives.Add(monthYear);
}
}
// load dropdown lists
DropDownListCategories.Items.Add("All");
foreach (string cat in categories)
{
DropDownListCategories.Items.Add(cat);
}
DropDownListAuthors.Items.Add("All");
foreach (string aut in authors)
{
DropDownListAuthors.Items.Add(aut);
}
DropDownListArchives.Items.Add("All");
foreach (string arc in archives)
{
DropDownListArchives.Items.Add(arc);
}
});
}
protected void FilterList()
{
ArticleRepository repo = new ArticleRepository();
List<Article> articles = repo.GetArticles();
Articles = new List<Article>();
foreach (Article article in articles)
{
bool select = true;
string category = DropDownListCategories.SelectedValue;
string author = DropDownListAuthors.SelectedValue;
string archive = DropDownListArchives.SelectedValue;
if (!category.Equals("All"))
{
select = article.Category.Equals(category);
}
if (select && !author.Equals("All"))
{
select = article.Author.Equals(author);
}
if (select && !archive.Equals("All"))
{
string monthYear = article.Date.ToString("MMMM yyyy");
select = archive.Equals(monthYear);
}
if (select) Articles.Add(article);
}
}
protected void DropDownListCategories_SelectedIndexChanged(object sender, EventArgs e)
{
FilterList();
}
protected void DropDownListAuthors_SelectedIndexChanged(object sender, EventArgs e)
{
FilterList();
}
protected void DropDownListArchives_SelectedIndexChanged(object sender, EventArgs e)
{
FilterList();
}
}
}
In the Page_Load event I load the article list. I also start an
async task to load the drop down lists with the unique author, category,
and month values from the list. The page then loops through the list of
articles and displays a link for each one. If one of the drop downs is
selected the article list is filtered and the page is rebuilt based on the
newly filtered list.
When the user selects an article the article detail page is
displayed. The article Id is passed to this page as part of the
URL. The body of the article is pre-formatted as HTML and stored in
a CDATA block withing the xml file. So this page just retrieves the
specified article from the repository and displays it.
Here's the detail page:
<%@ Page Title="Article Detaiils" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="ArticleDetails.aspx.cs" Inherits="MyWebSite.ArticleDetails" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div class="jumbotron shadow">
<div class="text-center">
<h1><%=post.Title %></h1>
<p class="lead"><%=post.Summary%></p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<%=post.Content %>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
Written by <%=post.Author%> on <%=post.Date.ToString("MM/dd/yyyy") %>
</div>
</div>
</asp:Content>
And this is the code behind:
using MyWebSite.Classes;
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
namespace MyWebSite
{
public partial class ArticleDetails : System.Web.UI.Page
{
public Article post;
protected void Page_Load(object sender, EventArgs e)
{
post = new Article();
string strId = Request.QueryString["id"];
if (strId != null)
{
if (int.TryParse(strId, out int id))
{
ArticleRepository repo = new ArticleRepository();
Article foundPost = repo.FindArticle(id);
if (foundPost != null) post = foundPost;
// add description metadata
HtmlMeta meta = new HtmlMeta
{
Name = "description",
Content = post.Summary
};
this.Page.Header.Controls.Add(meta);
// add keywords metadata
meta = new HtmlMeta
{
Name = "keywords",
Content = post.Title
};
this.Page.Header.Controls.Add(meta);
Page.Title = post.Title;
}
}
}
}
}
These pages are only for display of the articles. For now I use a simple
text file editor to create the articles and store them in the XML file. I
may decide to build an editor page at some point in time, but for now this
rudimentary system meets my needs.
In the interest of completeness I am including a sample xml file.
<?xml version="1.0"?>
<ArrayOfArticle xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Article>
<Id>1</Id>
<Title>Sample Article Title</Title>
<Author>Me</Author>
<Date>2020-07-17T15:04:41.1463148-04:00</Date>
<Category>Web Development</Category>
<Summary>Summary for a sample article</Summary>
<Content>
<![CDATA[<p>This is a sample article</p>]]>
</Content>
</Article>
</ArrayOfArticle>
I hope you find this useful. And in case you were wondering, this
technique was how I built the page you are reading right now.