Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An XML based (we)blog with RSS feed

0.00/5 (No votes)
7 Jul 2003 1  
A BLOG tool ready to use. Post weblogs to an XML file from a windows application via Web Service. Uses SOAP Headers for authentication. Uses simple XSL Transformation for the RSS feed.

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here