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
="1.0" ="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:
="1.0" ="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
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
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
End Try
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()
Dim dvWeblog As DataView = dsWebLog.Copy.Tables(0).DefaultView
If dateFilter <> "" Then
dvWeblog.RowFilter = "logtime < '" & dateFilter & "'"
End If
dvWeblog.Sort = "logtime desc"
Dim dtWeblog As DataTable = XmlHelper.GetTopRows(dvWeblog,
ConfigurationSettings.AppSettings("maxrows"))
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...
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, RichTextBox
for 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
Dim wsWeblog As New Weblog.Weblog()
Dim password As String = ConfigurationSettings.AppSettings("password")
If password Is Nothing Then
ConfigError("password")
End If
Dim authentication As New Weblog.AuthHeader()
authentication.password = password
wsWeblog.AuthHeaderValue = authentication
Dim URL As String = ConfigurationSettings.AppSettings("wsPostURL")
If URL Is Nothing Then
ConfigError("URL")
End If
wsWeblog.Url = URL
wsWeblog.PostMessage(TextBox1.Text, _
RichTextBoxUtil.ConvertToHTML(RichTextBox1))
WindowState = FormWindowState.Minimized
HideMe()
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
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")
drNew.Item("logtext") = message
dsWebLog.Tables(0).Rows.InsertAt(drNew, 0)
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.