Attention: We are retiring the ASP.NET Community Blogs. Learn more >

[ASP.NET] An XML based (we)blog with RSS feed

This is an old article I wrote for CodeProject some time ago. I'm putting it here in my blog to keep track of it. The code is avaliable for download from CodeProject -> http://www.codeproject.com/soap/weblog.asp

Sample Image - weblog.gif

Introduction

Since writing weblogs, or blogging as it is also called, has become pretty popular the last year, I thought of constructing my own blog tool. Blog is a shorter word for a web log, an online (most often public) journal where the author writes down his or her thougths, sometimes around a specific topic. This article describes how to write a pretty simple weblog application and a windows program for writing entries sitting in the system tray.

Some of the techniques used in this application are XML and XML Schema, Web Services, DataSets, Cache and the Calendar Web Control. Oh, and the XML Control too for transforming the XML weblog into RSS.

The Web Application

The web application consists of three parts actually; the web page showing the log and a calendar, a password protected web service for accepting entries and finally a second web page, which transforms the internal XML file into a RSS 2.0 feed via XSL transformation.

The Windows Application

The windows application (from now on called the client) is fairly simple in functionality and consists of a single dialog where the user can type in a message and send it over to the web site via a Web Service call.

The client sits in the system tray all the time, and when the user wants to write a message in his or her weblog, a click with the mouse brings up the dialog, ready for use.

Using the code

Let’s go over some of the more interesting parts of the code, starting with the XML format for the weblog data.

The Weblog XML and Schema

<?xml version="1.0" standalone="yes"?> <weblog> <logentry>     <id>0a8d4ec3-eec1-4b07-b26f-98bb5561f43c</id>     <logtitle>A title</logtitle> <logtime>2003-01-10T13:28:14.2031250+01:00</logtime> <logtimeGMT>Fri, 10 Jan 2003 13:28:14 GMT</logtimeGMT> <logtext>This is an entry in the weblog.</logtext> </logentry> </weblog> 

And the XML Schema for the weblog:

<?xml version="1.0" encoding="utf-8" ?> <xs:schema id="weblog" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:element name="weblog" msdata:IsDataSet="true" msdata:Locale="sv-SE"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="logentry"> <xs:complexType> <xs:sequence>               <xs:element name="id" type="xs:string" minOccurs="0" />               <xs:element name="logtitle" type="xs:string" minOccurs="0" /> <xs:element name="logtime" type="xs:date" minOccurs="0" /> <xs:element name="logtimeGMT" type="xs:string" minOccurs="0" /> <xs:element name="logtext" type="xs:string" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema> 

As the XML and the schema shows, the weblog consists of a number of log entries containing data for id, logtitle, logtext, logtime and logtimeGMT. The logtimeGMT is for the RSS feed, since it needs to be in RFC 822 format. I couldn’t find any simple way of transforming logtime into GMT with XSLT so I took the lazy path and stored both of them in the XML file. The id tag is a unique id that is given to each new blog entry.

The weblog web page

The weblog is presented on the web page by reading the XML file into a DataSet and binding that to a Repeater. I like the Repeater for simple loops like this, why use the more complex DataGrid or DataList when it’s not needed?

Remember to turn off the ViewState of the Repeater, it’s not needed and will speed up the loading of the page.

Every call to the page starts by getting the cached DataSet from the XML file. This is done in the Page_Load event.

Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load 'allways start with getting our cached dataset dsWebLog = XmlHelper.GetDS() If Not IsPostBack Then SetDates() BindList() End If End Sub 

The XmlHelper class has a few static methods for reading and writing the XML DataSet.

The location of the XML file is stored in the ASP.NET configuration file, web.config.

Public Shared Function GetDS() As DataSet 'get DS from cache Dim ds As DataSet = CType(HttpContext.Current.Cache("dsWebLog"), DataSet) If ds Is Nothing Then ds = New DataSet("weblog") ds.ReadXmlSchema(ConfigurationSettings.AppSettings("xmlSchema")) Try ds.ReadXml(ConfigurationSettings.AppSettings("xmlFile")) Catch ex As Exception 'missing an xml file is perfectly fine, this might be the 'first time the app is used End Try 'store in cache with dependency to the xml file HttpContext.Current.Cache.Insert("dsWebLog", ds, _ New Caching.CacheDependency(ConfigurationSettings.AppSettings("xmlFile"))) End If Return ds End Function

The cache has a dependency to the XML file, so the .NET Cache will automatically flush the cached DataSet if a new message is added to the XML file.

To be able to select a certain date, I also added the ASP.NET Calendar control to the page. When the page is loaded I loop through all the dates in the weblog XML DataSet and select all the dates in the calendar that has an entry in the weblog. When someone clicks a certain date in the calendar, the DataSet is filtered before it’s bound to the Repeater.

Private Sub Calendar1_SelectionChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Calendar1.SelectionChanged dateFilter = Calendar1.SelectedDate.AddDays(1).ToString SetDates() BindList() End Sub 

Before the DataSet is bound to the Repeater, the log entries are sorted and only the top 50 entries are shown. This (as so much else in the sample app) can be set in the web.config file.

 Private Sub BindList() 'get a dataview from a copy of the cached dataset Dim dvWeblog As DataView = dsWebLog.Copy.Tables(0).DefaultView 'filter on date? If dateFilter <> "" Then dvWeblog.RowFilter = "logtime < '" & dateFilter & "'" End If 'sort it by date and time dvWeblog.Sort = "logtime desc" 'copy maximum nr of rows to show Dim dtWeblog As DataTable = XmlHelper.GetTopRows(dvWeblog, ConfigurationSettings.AppSettings("maxrows")) 'bind the sorted and stripped log to the repeater weblogList.DataSource = dtWeblog weblogList.DataBind() End Sub 

The DataSet is filtered by setting the RowFilter property of the DataView. The .NET Cache has a pointer to our cached DataSet, and the cached DataSet has a pointer to the DataView, so if we don’t take a copy of the DataSet, the RowFilter property will be the same for other users of the cached DataSet. Something I discovered the hard way...

 'get a dataview from a copy of the cached dataset Dim dvWeblog As DataView = dsWebLog.Copy.Tables(0).DefaultView 

The method called GetTopRows is also located in the XmlHelper class, and it copies a specific number of rows from the log to be displayed in the page.

 Public Shared Function GetTopRows(ByVal dv As DataView, _ ByVal Rows As Integer) As DataTable Dim dtReturn As DataTable Dim cRow As Integer Dim maxRows As Integer maxRows = dv.Count dtReturn = dv.Table.Clone() For cRow = 0 To (Rows - 1) If cRow = maxRows Then Exit For dtReturn.ImportRow(dv(cRow).Row) Next Return dtReturn End Function

The weblog client

The client is made up from a single dialog, which starts up minimized to the system tray, i.e. as an icon in the status area of the desktop. The dialog has a TextBox for the title, RichTextBoxfor the body text and a couple of buttons for sending the log entry to the Web Service and for hiding or closing the program.

So, to post some text to the weblog Web Service, the user types some text in the title textbox and in the body textbox, then presses the Send-button. I thought the Web Service should have some way of protection, so therefore the call is authenticated with a password sent in the SOAP Header. The password is stored in a config file, and I use the built in .NET ConfigurationSettings file (WeblogClient.exe.config) for this.

Update: To be able to type in formatted text with different colors and fonts, and also to be able to type in HTML or XML tags, the text in the RichTextBox is first converted to HTML (RichTextBoxUtil.ConvertToHTML()). You can have a look at the utility class called RichTextBoxUtil.vb to see how it is done. Note that the utility doesn't handle links yet.

 Private Sub Send_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles SendButton.Click 'Send message to the Weblog via SOAP/Webservice Dim wsWeblog As New Weblog.Weblog() 'get password from the config file (WeblogClient.exe.config) Dim password As String = ConfigurationSettings.AppSettings("password") If password Is Nothing Then ConfigError("password") End If 'this is our SOAP Header class Dim authentication As New Weblog.AuthHeader() authentication.password = password wsWeblog.AuthHeaderValue = authentication 'get the Web Service URL from the config file Dim URL As String = ConfigurationSettings.AppSettings("wsPostURL") If URL Is Nothing Then ConfigError("URL") End If 'set the correct URL for the Web Service wsWeblog.Url = URL         'send HTML converted text to the weblog webservice         wsWeblog.PostMessage(TextBox1.Text, _ RichTextBoxUtil.ConvertToHTML(RichTextBox1)) WindowState = FormWindowState.Minimized HideMe() 'clear out the textbox Me.RichTextBox1.Clear()         Me.TextBox1.Clear() End Sub 

The URL for the Web Service is also stored in the config file (WebLogClient.exe.config), which must be located in the same directory as the weblog client.

The Web Service

The Web Service method for receiving and storing the posted message is quite small. It's one simple method, and it first checks the SOAP Header and compares the password, then it stores the posted message to the weblog.

<WebMethod(Description:="Post a message to the weblog. An authentication SOAP header is mandatory."), SoapHeader("authentication")> _ Public Function PostMessage(ByVal title As String, ByVal message As String) As Integer If authentication.password = ConfigurationSettings.AppSettings("password")_ Then 'password is ok, stor message in the XML file XmlHelper.AddMessage(title, message) Else Throw New Exception("Invalid password") End If End Function

The password is (as so much else) stored in the web.config file.

The AddMessage() method just adds a new DataRow in the weblog DataSet and saves it back to XML. The method also creates a unique id for this posting. The new DataRow is added at the top of the DataSet. The XML file is stored at the location specified by the web.config file (default is at c:\weblog.xml).

 Public Shared Sub AddMessage(ByVal title As String, _ ByVal message As String) Dim dsWebLog As DataSet = XmlHelper.GetDS Dim drNew As DataRow drNew = dsWebLog.Tables(0).NewRow         drNew.Item("id") = Guid.NewGuid.ToString         drNew.Item("logtitle") = title drNew.Item("logtime") = Now drNew.Item("logtimeGMT") = Format(Now, "r") 'RFC 822 format  drNew.Item("logtext") = message dsWebLog.Tables(0).Rows.InsertAt(drNew, 0) 'insert it at beginning 'store xml again dsWebLog.WriteXml(ConfigurationSettings.AppSettings("xmlFile")) End Sub 

The weblog RSS 2.0 feed

More and more of the weblogs on the Internet provide an RSS feed of it’s content. I've seen different explanations about what RSS stands for. This was taken from the RSS specification:

“RSS is a Web content syndication format. Its name is an acronym for Really Simple Syndication. RSS is dialect of XML.”

But some people say "RDF Site Summary", and RDF stands for Resource Description Framework, which is a foundation for processing metadata. It really doesn't matter, it's a great way to publish content in a simple XML way.

RSS has been around since 1999 and I’ve tried to create a very simple RSS feed by reading the RSS 2.0 Specification located at http://backend.userland.com/rss

Just for the “fun” of it, I tried to use XSL Transformation to turn the weblog XML file into the correct RSS format. So, I created a new WebForm ASPX page, and removed everything except the Page header from it, and added a ContentType attribute to it for text/xml.

<%@ Page contenttype="text/xml" Language="vb" AutoEventWireup="false" Codebehind="rss20.aspx.vb" Inherits="Weblog.rss20"%> 

Then I drag/dropped an ASP.NET XML Control to the page and added some code in code-behind to point out the XML file and the XSL file.

Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Dim doc As XmlDocument = New XmlDocument() doc.Load(ConfigurationSettings.AppSettings("xmlFile")) Dim trans As XslTransform = New XslTransform() trans.Load(ConfigurationSettings.AppSettings("RSSxslFile")) Xml1.Document = doc Xml1.Transform = trans End Sub 

This is the XSL file used to transform the XML file:

<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:template match="/"> <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> <title>My weblog</title> <link>http://localhost/weblog/</link> <description>Just another weblog...</description> <managingEditor>someone@somewhere.com (My Name)</managingEditor> <language>en-us</language> <xsl:for-each select='weblog/logentry'> <item> <link>http://localhost/weblog/Item.aspx?id=<xsl:value-of select='id'/></link>             <guid isPermaLink="false"><xsl:value-of select='id'/></guid>             <title><xsl:value-of select='logtitle'/></title>             <description><xsl:value-of select='logtext'/></description>             <pubDate><xsl:value-of select='logtimeGMT'/></pubDate> </item> </xsl:for-each> </channel> </rss> </xsl:template> </xsl:stylesheet> 

The XSL file loops through each log-entry and writes them out within description and pubDate tags. Publication date needs to be in RFC 822 format (GMT-format) according to the RSS spec, that’s why I use that field in the XML file.

Update: The XSL file has been updated now so is also writes out a title, guid and a link to the blog entry.

One bad thing with this page is that it will write out every record in the weblog, something I took care of in the web page. It shouldn’t be too hard to sort and filter out the top 50 records or so in the way it’s done in the web page, but I leave that for later updates.

Points of interest

I could have created the RSS feed in a number of different ways, but I’ve always wanted to try out the ASP.NET XML Control, so that’s why I went for that. I found out that you can do a number of things with XSL Transformation, but wow, it’s pretty complicated.

As I wrote earlier in the article, it’s easy to forget that the .NET Cache keeps pointers to reference type objects and if you change data in objects you get from the Cache, you are actually changing the object kept in the .NET Cache. Remember that when using the Cache object, you might mess things up for other visitors to the webby. As long as you store value types in the Cache you don’t have to worry.

Updates

Update 1 - I added a title to the blog entry, mostly because it looks best i different RSS readers if there's a title for each blog entry. For the sake of RSS, I also added a guid for each entry. I also added code to transform some of the formatted text in the RichTextBox into HTML. It shouldn't be any problems to cut and paste colored and indented source code or HTML/XML into the RichTextBox. It looks pretty good on the HTML page. Note that it doesn't handle hyperlinks yet.

No Comments