Roughly a year ago I seperated nearly all of the site's content
out from the presentation layer and from the code, placing it
in XML files. One of the benefits to this approach was that I
could allow users to leverage all of the site's content from a
single dataset -- this is precisely what happens on the search
web form. Any new content added to the application becomes
immediately searchable, and I no longer have to recompile and
redeploy the application because I have something new on my
nightstand.
Of course, telling you I have something new on my nightstand takes
several forms. The primary, and most detailed means, is the content
in the interests section of the application. A secondary means is
by writing some brief remarks on the welcome web form and in the
feed which is picked up by the RSS screensaver. Historically, I've
accomplished these tasks by writing the "interests" content within
a new XML node, then writing summarizing remarks inside two
additional nodes in separate files. Although I could take some
advantage of cut-and-paste, I'd still have to modify even that
content further because the screensaver software can't parse HTML.
The tedium of editing three different files recently fueled serious
interest in creating an automated means of introducing (meaning both
adding and telling you about) content changes on the site.
Over the past few weeks, I've modified the application in two major
ways:
(1) The "welcome" web form is now fed by a content dataset similar
to that at work on the search form; it displays all content entries
associated with the date appearing in the most recent record (e.g.,
if three content entries were made today, March 7th 2009, then
only information about those three entries would be displayed);
(2) The XML file which feeds the RSS screensaver is now generated
automatically.
AUTOMATED WELCOME
I used the same premise as I had when building the site's search
capability: read all available content into a single, page-level
dataset:
Private Function LoadDataSet() As DataSet
Dim dsDataSet As New DataSet
'create a reader object and point it to the xml doc
Dim rdrInterests As New XmlTextReader(Server.MapPath("interests.xml"))
Try
'import the reader's data into the dataset
dsDataSet.ReadXml(rdrInterests)
Catch ex As Exception
'handle exceptions here
Finally
'close the reader
If Not rdrInterests.ReadState = ReadState.Closed Then
rdrInterests.Close()
End If
End Try
'create a reader object and point it to the xml doc
Dim rdrEducation As New XmlTextReader(Server.MapPath("education.xml"))
. . .
Once all of the xml documents have been read into the dataset,
the dataset is passed back to the page, where it may be consumed
by other subroutines and functions.
'Latest News
If Not dsDataSet Is Nothing AndAlso dsDataSet.Tables.Count > 0 Then
. . .
First, I create a dataview from the dataset, with some filters
and sorting in place:
'create a dataview from the dataset
Dim dvDataView As New DataView(dsDataSet.Tables(0))
With dvDataView
'filter out the inactive items
.RowFilter = "active = 'True' "
'sort on date (most recent at top)
.Sort = "pubDate DESC"
End With
Next up, start looping through the view to parse out the date.
Recall that, for "latest news," we want everything that is
associated with the most recent date only.
For intCount = 0 To intUpperBound
. . .
'parse out the date
If dvDataView(intCount)("date").ToString.Length > 0 Then
strCurrentItem = String.Format("{0:d}", dvDataView(intCount)("date"))
If IsDate(strCurrentItem) Then
datCurrentDate = CDate(strCurrentItem)
Right now, I'm using a date limit, which I'll probably end up
ditching. At the very least, it pushes me to put new content
on the site within every fifteen days :-)
timeSpan = Now.Subtract(datCurrentDate)
If timeSpan.Days < constDateLimit Then
With datCurrentDate
intMonth = .Month
intDay = .Day
intYear = .Year
End With
If the latest date is within our limit, use the StringBuilder
object to build content (I'm using a With statement which
refers to the stringbuilder):
'loop through adding data
.Append("")
If Len(dvDataView(intCount)("linkTitle").ToString()) > 0 Then
.Append(dvDataView(intCount)("linkTitle").ToString())
. . .
.Append(": ")
I should clarify something at this point. See, having the code
just grab the first n characters of my content for each entry
is kinda dumb. So I'm not entirely getting out of writing
additional content. After some thought, I found my best solution
was to add an element to my schema. The summary node contains
the brief synopsis I would have otherwise written for the "latest
news".
If Len(dvDataView(intCount)("summary").ToString()) > 0 Then
.Append(dvDataView(intCount)("summary").ToString())
. . .
End If 'Len(dvDataView(intCount)("summary").ToString()) > 0
Note the If-Then statement above wants to know the length of that
summary content. Inside the ellipses above is actually the "else"
code: if there's no summary, fall back to grabbing the first n
characters of the content. If the length of the content exceeds n,
then add ellipses to the string and punt.
.Append("
")
' close table after last entry
If intCount = intUpperBound Then .Append("
|
")
End If 'timeSpan.Days < constDateLimit
End If 'IsDate(strCurrentItem)
End If 'dvDataView(intCount)("date").ToString.Length > 0
Next 'For intCount = 0 To intUpperBound
We'll pick up in the next section where this code leaves off.
SCREENSAVER
The software I used to build the real simple syndication (RSS)
screensaver requires any RSS feed the screensaver is intended to
consume originate from an XML file. That is, the feed cannot
originate from from a dynamic RSS source (for example, a .NET web
form). Therefore, I must maintain a .xml to feed the screensaver.
For over a year, I manually coded content changes into RSS XML.
I promised myself that, as time permitted, I would seek to build
the source file dynamically.
The generator code takes as input the path of the .xml file, a title
for the element (usually "Updates" or some such), and finally the
content.
Public Shared Function CreateXmlForRss(ByVal strFilePath As String, ByVal strElementTitle As String, ByVal strElementDescription As String) As Integer
'called from welcome.aspx.vb. Code on the web form plants a flag containing the status this function returns;
'if the flag is not present (the status code doesn't matter), the web form calls this function. If a flag is
'present, then at least an attempt was made at the start of the session, and no update attempt should occur
'for the remainder of the session.
Dim strTime As String = Now.ToUniversalTime
Dim xwSettings As New XmlWriterSettings()
. . .
First, we must configure a new XmlWriterSettings() object:
With xwSettings
.Indent = True
.NewLineOnAttributes = False
End With
That done, we create an XmlWriter object and set about constructing our
XML. Note, though, that I've wrapped a Try-Catch-Finally block around
this, in order to handle a possible System.UnauthorizedAccessException.
Such an exception will occur if your application does not have
permissions to write to the XML document (learned this the hard way).
Try
Using xmlWriter As XmlWriter = xmlWriter.Create(strFilePath, xwSettings)
With xmlWriter
.WriteStartDocument()
.WriteStartElement("rss")
.WriteStartAttribute("version")
.WriteValue("2.0")
.WriteEndAttribute()
.WriteStartElement("channel")
.WriteStartElement("title")
.WriteValue(strTitle)
.WriteEndElement() 'title
. . .
.WriteStartElement("description")
.WriteValue(strElementDescription)
.WriteEndElement() 'description
.WriteStartElement("pubDate")
.WriteValue(strTime)
.WriteEndElement() 'pubDate
.WriteEndElement() 'item
.WriteEndElement() 'channel
.WriteEndElement() 'rss
.WriteEndDocument()
.Flush()
End With
End Using
Return 0
Catch excAccess As System.UnauthorizedAccessException
'this exception occurs when the file system denies access to the app to write to the specified file.
'the file should be \xml\xml_rss.xml
Return 1
End Try
End Function
So there we have it: our actual generator. Now we need to determine when
to trigger it. On the same web form as that from which I quoted earlier,
I have a subroutine called UpdateRSS(). Note that the third DIM statement
calls the CreateXmlForRss() function listed above. As you can tell from
the statement, it's expecting an integer to be returned from the function.
Private Sub UpdateRSS(ByVal strElementDescription)
Dim strElementTitle As String = "Updates"
Dim strFilePath As String = Server.MapPath("syndication.xml")
Dim intRssXmlCreationStatus As Int32 = CreateXmlForRss(strFilePath, strElementTitle, strElementDescription)
When I first put this code in place, I was having the RSS XML re-written
upon every visit to the "Welcome" web form. That's completely unnecessary.
A better way to do business might be to update it once per day, or maybe
once per session. At this point, I'm doing this check once per session --
the very first time a visitor loads welcome.aspx.
'plant a flag in session with the status
HttpContext.Current.Session.Add("blnRssXmlUpdated", intRssXmlCreationStatus.ToString())
With lblRssXmlUpdateStatus
.Text = "RSS content "
I've created an enum for the possible statuses. A default, an OK, and my
one exception -- the unauthorized access exception. I'm having a status
message on welcome.aspx updated with text which indicates the return code.
Select Case intRssXmlCreationStatus
Case RssXmlCreationStatus.StatusDefault
.Text &= "update has not run."
Case RssXmlCreationStatus.StatusOK
.Text &= "updated " & Now()
Case RssXmlCreationStatus.StatusException_UnauthorizedAccess
.Text &= "update exited with a status code of 1."
Case Else
End Select
'make sure the message may be seen
.Visible = True
End With 'With lblRssXmlUpdateStatus
End Sub
Returning to where we left off in the code for the automated welcome: If
there's nothing stored in session (this status is the only thing I store
in session in the application). I need a smarter way to do this logic,
but here's the best my poor brain could muster after wrestling with
income taxes all morning:
'**************************************************************************
'2009-03-07 rebuild xml\xml_rss.xml if our key/item isn't stored in session
'**************************************************************************
If HttpContext.Current.Session.Count = 0 Then
If strExportToRSS_ElementDescription.Length > 0 Then
Call UpdateRSS(strExportToRSS_ElementDescription)
End If 'strExportToRSS_ElementDescription.Length > 0
Else
'check for our key
Try
If Len(HttpContext.Current.Session.Item("blnRssXmlUpdated").ToString()) = 0 Then
'call the updater
If strExportToRSS_ElementDescription.Length > 0 Then
Call UpdateRSS(strExportToRSS_ElementDescription)
End If 'strExportToRSS_ElementDescription.Length > 0
Else
'if anything was found under the key, hide the status label
'and do not call the updater
With lblRssXmlUpdateStatus
.Text = String.Empty
.Visible = False
End With
End If 'strExportToRSS_ElementDescription.Length > 0
Catch ex As Exception
'my guess is the key doesn't exist... so call the updater
If strExportToRSS_ElementDescription.Length > 0 Then
Call UpdateRSS(strExportToRSS_ElementDescription)
End If 'strExportToRSS_ElementDescription.Length > 0
About that exception... I was expecting the app to freak out if it went
looking for the key and didn't find it. It surely would have had we been
in classic ASP. So, as a precaution, I embedded the logic in a Try-Catch-
Finally block.
End Try
End If 'HttpContext.Current.Session.Count = 0
So now we have our automagic "latest news" summary, complete with bonus
XML-writing RSS support. If I could find another RSS scrensaver software,
I might consider dumping this one... but at least now, with both mini-projects
out of the way, the addition of a short summary node to my schema saves
me the time of editing two additional XML documents. The code does it for me!
AUTOMATED WELCOME, IMPROVED
The only thing about this that I didn't entirely like was that if I failed
to post any new content within the timespan -- which is set at 15 days --
then no "latest news" would display. I wanted to tweak that a bit so that
if there's no content in that 15-day window, it'd grab whatever the last
content was and display it.
For this added functionality, I wanted to know two things: (1) Is there any
content in the window? and (2) what is the date of the most recent content?
Answering these questions became the job of a pair of functions:
GetLatestNewsCount() and GetLatestNewsPubDate().
The two functions are nearly identical, and should look very similar to
previous code:
Private Function GetLatestNewsCount() As Int32
'2009-07-21 return the count of latest news items that fall inside the cutoff window
Dim intLatestNewsCount As Int32 = 0
Dim intCount As Int32 = 0
Dim timeSpan As TimeSpan
Dim strCurrentItem As String = String.Empty
Dim datNull As Date = #1/1/1900#
Dim datCurrentDate As Date = datNull
If Not dsDataSet Is Nothing AndAlso dsDataSet.Tables.Count > 0 Then
'create a dataview from the dataset
Dim dvDataView As New DataView(dsDataSet.Tables(0))
With dvDataView
'filter out the inactive items
.RowFilter = "active = 'True' "
'sort on date (most recent at top)
.Sort = "pubDate DESC"
End With
Dim intUpperBound As Int32 = dvDataView.Count - 1
For intCount = 0 To intUpperBound
'parse out the date
If dvDataView(intCount)("date").ToString.Length > 0 Then
strCurrentItem = String.Format("{0:d}", dvDataView(intCount)("date"))
If IsDate(strCurrentItem) Then
datCurrentDate = CDate(strCurrentItem)
timeSpan = Now.Subtract(datCurrentDate)
If timeSpan.Days < constDateLimit Then
intLatestNewsCount += 1
Else
Exit For
End If 'If timeSpan.Days < constDateLimit
End If 'IsDate(strCurrentItem)
End If 'dvDataView(intCount)("date").ToString.Length > 0
Next 'For intCount = 0 To intUpperBound
End If 'Not dsDataSet Is Nothing AndAlso dsDataSet.Tables.Count > 0
Return intLatestNewsCount
End Function
Armed first with the knowledge of how many content posts are within
our display window, we can revisit the original code.
Up where the dataview is instantiated, dimension a variable,
assign it the Int32 type, and equate its value to the output of
GetLatestNewsCount(). Now just below where the .RowFilter property
is assigned, check that count, and instantiate a new string variable
equating to the value of GetLatestPubDate() -- then simply append
the value to the .RowFilter:
With dvDataView
'filter out the inactive items
.RowFilter = "active = 'True' "
'2009-07-21
'if there are no items that fall within range of the "latest news" items...
If intLatestNewsCount = 0 Then
'... get the pubDate string value of the last news story (e.g., '20090721') ...
Dim strLatestNewsPubDate As String = GetLatestNewsPubDate()
'... and append that date to the rowfilter.
.RowFilter &= " AND pubDate = '" & strLatestNewsPubDate & "'"
End If
'sort on date (most recent at top)
.Sort = "pubDate DESC"
End With
Almost done. One final tweak needs to be made to the code in the loop
which compares the current item's date value to the timeSpan object.
The current code compares the datelimit with timeSpan.Days, like this:
If (timeSpan.Days < constDateLimit) Then
With datCurrentDate
intMonth = .Month
intDay = .Day
intYear = .Year
End With
. . .
Because we already know, if our latest news count is zero, that no row
in that dataview will satisfy that condition, we need to give that If
statement a little help:
'2009-07-21 make allowance if the latest news count is zero
If (timeSpan.Days < constDateLimit) Or (intLatestNewsCount = 0) Then
With datCurrentDate
intMonth = .Month
intDay = .Day
intYear = .Year
End With
That's it -- the rest takes care of itself. The net result is that the
latest data you had on display will effectively stay displayed until
new content is added.
That should be it for now! Contact me using the site's contact form if
you have questions. Feel free to use the code in your projects. A shout
out in your project would be thoughtful. Also, drop me a line and let me
know how you might have tweaked things to better suit your needs.
Finally, I wouldn't profess to be THE expert on matters represented in
my code -- so drop me a line if you have constructive suggestions, too.
I'd like to hear from you!
Best,
halfgk
copyright 2009 halfgk.com