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