full source code.
this web application in action.
In this article we will take a look at how to create a newsgroup browser in ASP.Net. Newsgroup
articles are kept on the news server in order of arrival, not in order of message thread or subject.
But rather than display articles this way, we'll of course want to "thread" these messages so that
replies will fall under their parent message in that nice threaded tree view that we're all used to.
To do this, we'll download a large number of message headers and sort them out to the correct order.
This can be done very easily and conveniently with the XMLDocument Object in VS.Net.
In order to handle the interaction with the news (NNTP) server, we'll use the NNTP component in the
IP*Works! Internet Toolkit (.Net Edition). This will greatly simplify the project, and we'll be able
to finish in no time.
Connect to NNTP Server and Download Message Headers
The first step is to download the message details of however many messages you'd like to thread. To
do this with the component, specify the news server, the news group, and the range of articles you'd
like to download. For this demo, we'll just download the newest 100 articles in the group. Setting
the news server and news group will populate the lastarticle property with the message number of
the last article in the group. So to get the last 100 messages, we'll set the range to
nntp1.lastarticle-100 through nntp1.lastarticle. After we've done all of this, use the GroupOverview
method to perform an "overview" of the group (see code below). This will fire a "GroupOverview" event for each message
in the range we have specified. This event will tell us important details about each message which
we'll use to create our tree with the XMLDocument object.
Private Sub GetThreads(ByVal group As String)
Try
Nntp1.NewsServer = "msnews.microsoft.com"
Nntp1.CurrentGroup = "microsoft.public.dotnet.languages.vb"
Nntp1.OverviewRange = CStr(Nntp1.LastArticle - 100) & "-" & Nntp1.LastArticle
Nntp1.GroupOverview()
Nntp1.Disconnect()
Catch ee As Exception
Response.Write("GetThreads(): " + ee.Message)
End Try
End Sub
|
Creating The Tree
The GroupOverview event will provide us with the following details about each article in the range:
ArticleNumber - contains the number of the article within the group.
Subject - contains the subject of the article.
From - contains the email address of the article author.
ArticleDate - contains the date the article was posted.
MessageId - contains the unique message id for the article.
References - contains the message ids for the articles this article refers to (separated by spaces).
ArticleSize - contains the size of the article in bytes.
ArticleLines - contains the number of lines in the article.
OtherHeaders - contains any other article headers the news server provides for the article.
The most important part for us at this point is the messageid and the references. Every NNTP article
has its own unique message ID contained in the "Message-ID" header. Just so that you know what these
look like, here are some sample message ID's:
<e8ZlPf8rCHA.1132@TK2MSFTNGP12>
<vARP9.2130$>
<OKG$Dr8rCHA.2484@TK2MSFTNGP10>
<KV_P9.4450$>
<3E1086F3.2050805@.com>
Every NNTP article which is a reply also has a list of references contained in the "References" header.
If a new message is posted (the beginning of a thread), there is no references header. Each time a reply
is formed, its references header will contain all the references of the article it is a reply to (if there
are any) plus the message-ID of the article it is a reply to. For example:
[original message from Tom B.]
From: Tom B.
Subject: This is a new thread about smurfs
Message-ID: <>
[Sally C replies to Tom B., includes a references to his message]
From: Sally C.
Subject: Re[1]: This is a new thread about smurfs
Message-ID: <>
References: <>
[John D. replies to Sally C., includes Sally's references, plus her message id.]
From: John D.
Subject: Re[2]: This is a new thread about smurfs
Message-ID: <>
References: <>, <>
[Mike E. replies to Tom B., includes only a reference to his message]
From: Mike E.
Subject: Re[1]: This is a new thread about smurfs
Message-ID: <>
References: <>
|
It is these references and message-id's that we'll use to construct our tree. Each time the GroupOverview
event fires with these pieces of information, we'll look in our tree.
If the GroupOverview references parameter is empty, we know we have a new message which is NOT a reply.
In this case we can simply append a new node to the root of the tree, so the tree would look like:
-root
----- new message 1
----- new message 2
|
If the GroupOverview references is not empty, we know we have a new message which IS a reply. Now we
scan the tree to see if we can find the message thread that this new message belongs to. To do this,
we'll concentrate on only the last message-id in the references list, since that will be the message of
which this new message is a directly reply to. Traverse down the tree and search for that message-id.
If it is found, append a new node to the matched node, so the tree would now look like:
-root
----- new message 1
----- reply to message 1
----- new message 2
|
If you traverse the tree and do not find a match, then its safe to assume that the new message is in
reply to an old message (at least older than the newest 100 articles that we are looking at). So we'll
just start it as a new node at the root level:
-root
----- new message 1
----- new message 2
----- reply to old message
|
After all of our GroupOverview events fire, we'll have a large tree with all of the articles in it,
indexed so that replies are child nodes of the replied-to message. At this point the hardest part is
finished.
Private Sub Nntp1_OnGroupOverview(ByVal sender As Object, ByVal e As nsoftware.IPWorks.NntpGroupOverviewEventArgs) Handles Nntp1.OnGroupOverview
If e.References = "" Then
'this message has no references, it is not a reply, start a new thread:
'AddNode adds a new node to the XMLDocument object with the specified attributes.
'the last parameter is the node in the XMLDocument tree in which to append the new node.
AddNode(e.ArticleDate, e.Subject, e.From, e.MessageId, Msgs.ChildNodes(0))
Else
'this message refers to an earlier message
found = False
'GetLastReferenceID simply parses its parameter to return only the last message-id in it
Dim thisref As String = GetLastReferenceID(e.References)
'FindPlace is a recursive function which traverses down the tree looking for the node that
'contains the message-ID which equals the reference-ID we're looking for.
Dim place As System.Xml.XmlNode = FindPlace(thisref, Msgs.FirstChild)
If (found = True) Then
'found the place in the tree, add node to existing tree
AddNode(e.ArticleDate, e.Subject, e.From, e.MessageId, place)
Else
'did not find place in the tree, create new thread b/c reference is too old
AddNode(e.ArticleDate, e.Subject, e.From, e.MessageId, Msgs.ChildNodes(0))
End If
End If
End Sub
|
Displaying the Resulting Tree
Now we have a populated XMLDocument object. This can be traversed programmatically and displayed any
way you like. Instead I'm going to use the "XML" Webforms object of VS.Net. This object is used to
display XML data in a webforms application. It has a DocumentSource and a TransformSource property,
to which you assign xml data and xsl data respectively. I'll simply provide the DocumentSource property
with the OuterXML property of the XMLDocument tree, and use the TransformSource property with an XSL
file to display the data however I like.
Private Sub DisplayThreads()
Xml1.TransformSource = "xmlnewsthread.xsl" '(this is a relative URL)
Xml1.DocumentContent = Msgs.OuterXml
End Sub
|
My xmlnewsthread.xsl sheet displays the message in a tree structure. Each child node is displayed with
a left margin (in pixels) of 10 times its level in the tree. The subject and sender of each article is
displayed. The XSL looks like so:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="message">
<xsl:apply-templates select="message"/>
</xsl:template>
<xsl:template match="message">
<span>
<xsl:attribute name="style">margin-left:<xsl:value-of select="count(ancestor::*)"/>0px;</xsl:attribute>
<a><xsl:attribute name="href">read.aspx?ID=<xsl:value-of select="@msgid"/>&group=<xsl:value-of select="/newsgroup/@name"></xsl:value-of></xsl:attribute><xsl:value-of select="@subject"/></a></span>
by <span><xsl:attribute name="style">color:#008080;</xsl:attribute><xsl:value-of select="@from"/></span>
<br/>
<xsl:apply-templates select="message"/>
</xsl:template>
</xsl:stylesheet>
|