Click here to Skip to main content
13,138,857 members (56,594 online)
Click here to Skip to main content
Add your own
alternative version

Stats

7.8K views
4 bookmarked
Posted 21 May 2015

Part 2: CodeProject.Show DeepDive - exploring the source code behind

, 21 May 2015
Rate this:
Please Sign up or sign in to vote.
Exploring the source code behind the offline CodeProject article writer CodeProject.Show.

This article is a follow up article on the original article, Part 1: CodeProject.Show - A CodeProject offline article writer . That is the official page for source code updates and how to use this app. Please get your downloads there. In this article we take a deep dive into the source code making, no images, just the source code.

Introduction

The purpose of this article is to discuss the source code behind CodeProject.Show, an offline CodeProject article writer. I have been playing around this idea for a while now due to internet connectivity issues here in our beatiful country. I wanted to have an offline article writer that could replicate the same functionality as the CodeProject online writer. I wanted it to be easy to use and one to be able to write and store all their articles in one place. I have grown to like .Show prefixes in naming my apps. I guess I was inspired by Family.Show, a Vertigo family tree app. Only if it could work from the net. As I result, I tried to replicate a family tree using JQuery Mobile and D3 as explained in this article and this one.

Anyway to cut a long story short, I fired up Visual Studio and created a new project and called it CodeProject.Show for this very purpose. I needed a treeview to list all my articles, a WYSIWYG editor and also an HTML Editor. I decided that my back end would be SQLite, though that is not used much within the article and later decided that the articles will be stored in a single html file under an Articles folder within the application directory of CodeProject.Show.

Some silly assumptions: You have written an online CodeProject article before and are familiar with word processing and have read my CodeProject.Show article discussing the user of this here. We will be looking at the source code that brings the app to life here. You are also familiar with creating Visual studio Vb.Net applications. If you know C#, you can also convert the project using SharpDevelop.

Creating the VB.Net Project

  • I created a form in my VB.Project and set it up to ensure its centered and maximised on start on Project > Add Windows Form.
  • I added a StatusBar, ToolStrip, ImageList, FontDialog, ColorDialog and a Timer controls.
  • From the toolbox Container, I double clicked on the Split Containers
  • I dragged and dropped another Split container on Panel 2 of the existing panel
  • I dragged and dropped a TreeView on the first panel of the first split container. This is under Common Controls
  • I dragged and dropped a WebBrowser control on panel1 of the second split container.
  • Now I went to Tools > Extensions and Updates and searched for ICSharp.TextEditor and installed it to my project. Set this up on the toolbox and dragged it to panel2 of the second split container.
  • I ensured that the Dock position of all these three controls is FullDock.
  • I added references to SQLite, SQLServerCe, mshtml and the Speech libraries
  • I created the Toolbar buttons

Preparing my CodeProject Template

I downloaded the submission template file from CodeProject and made some changes to it. These are:

  • Remove Step 3 from the body section
  • Added this css to the header to take care of the Quote appearance with gray backgroud.
.quote {padding:0 10px 10px 27px;margin-left:.25em;color:#565;margin-right:1em;margin-bottom:1em;background:url("quote.gif") no-repeat scroll left top #eee}
  • Added this to the body (to ensure that the mouse pointer shows on the webbrowser control to enable online editing)
contentEditable='true'

I named the template article.txt, copied it to my VB project location and set it up to copy always when compiling. This will later be referenced by the code.

Preparing my article.db, an SQLite database

I wanted to have a record of my articles and each one allocated a unique number. I fired up Database.NET, a free software to administer databases. I love the ease of this app.

  • Using File > Connect > SQLite > Create, I created a new database directly at my project location.
  • After that I selected Tables > Right Click > Create Table > then + to add columns. I added ID (inteter, primary key, auto increment), article (for article name) etc.
  • Click Save, type in table name, I called it articles.

Code Helpers

I have written some classes before to help me manage my applications. These are Files (for anything that has to do with file manipulation), SQLite (for anything SQLite related), Map (Data Dictionary helper), Speech (anything to do with speech), clsWinForms (anything with WinForms apps but tweaked that to Common now). I copied these over to this project for use. I will explain the functionality used for CodeProject.Show soon enough.

I was ready to create the interface and write the code now.

Background

Developing an application like this took some research. Microsoft has a webcontrol that one is able to pass to it commands using ExecCommand. More information about these commands is available here. As this is the first version of this project in a WinForms applications, I must say one of the inspirations to create such an article writer came from this article here. With that in mind, after searching for similar articles, I decided one day I will just do this, and here is it. I must admit, I have never programmed a webcontrol to such detail before and am grateful that through google I have been able to consolidate most of what I learned about the webcontrol and its automation using ExecCommand into this app. The best part for me in all of this is. I know how and my passion to change the world is again realised.

You as a possible end user of this app are please welcome to make suggestions and recommendations. I could use some, even if its about enhancing this application because I believe it will add a lot of value to people's lives, just like it has me. The article you are reading now is conceptualized and created directly from CodeProject.Show.

As indicated in my links above, there is quiet a lot that you can achieve with ExecCommand using the webcontrol however some commands are not supported in most browsers, even internet explorer and thus I had to write some few code to make some functions work. For example, to wrap a selection of article text within a <code> element, I had to write a script like this below:

    Private Sub WrapSelection(elementX As String)
        ' this does the same as formatcode, when it does not work
        ' element to set the attribute
        Dim hElement As IHTMLElement
        ' get the document
        Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
        ' get selected range
        Dim range As IHTMLTxtRange = doc.selection.createRange()
        ' text block quote
        hElement = doc.createElement(elementX)
        hElement.innerHTML = range.htmlText
        range.pasteHTML(hElement.outerHTML)
    End Sub

where elementX could be anything e.g. "code", "div" etc.

To be able to use CodeProject.Show, one needs to understand its structure. The application is broken down into various sections. These are the following:

The toolbar - this provides most functionality to write and format your article

The treeview - this lists all your articles, you can click on an article to open it, delete it, rename it etc. This is toggable and you can hide and show it.

Article Writer - the middle portion of the screen where one writes their articles. This part uses the WebControl in design mode and contentEditable on. Your article is saved every 1 minute intervals and a backup done when you are writing it. Each article is saved as a html file using a unique article number.

HTML - the right portion of the screen is also toggable and that is where you can view the HTML source of your article. This section is read only.

Statusbar - at the bottom of CodeProject.Show is the status bar that gives one statistics about their document. This tells how many words are in your article, when was it last saved, the size of the folder containing your article details and its location.

The purpose of this article is about how I created this application. I will touch on the source code for the various sections now.

Using the code

1. Maintaining Articles with CodeProject.Show

Maintaining articles with CodeProject.Show involves, Creating a new Article, Deleting an article, Renaming an article, Resetting an article and Saving an artilcle as an html file.

1.1 File > New

This is to add a new article to your database of articles. You will be expected to type over the tree view item and type in your new article name and press Enter.

Private Sub cmdNewArticle_Click(sender As Object, e As EventArgs) Handles cmdNewArticle.Click
        ' add an article to the treeview
        Timer1.Enabled = False
        Dim pNode As TreeNode = treeArticles.Nodes("root")
        SelectedArticle = pNode.Nodes.Add("new", "New Article", "page", "page")
        ' ensure the treenode is visible
        SelectedArticle.EnsureVisible()
        ' ensure the treenode is the selected node for editing
        treeArticles.SelectedNode = SelectedArticle
        ' set the editing mode to true
        treeArticles.LabelEdit = True
        ' begin the edit of the node
        If Not SelectedArticle.IsEditing Then
            SelectedArticle.BeginEdit()
        End If
        Timer1.Enabled = True
    End Sub

A timer exists to save an article every minute. To add a new article, we turn the timer off first with timer1.enabled = false. When the app starts it loads all available articles in the treeview under a root node. All child articles are then added to that root. Each new article will have a name "New Article" and display an icon called "page" the icon is sourced from the imagelist that is linked to the treeview. This procedure then fires up LabelEdit and BeginEdit for the treeview to enable a user to type over the name of the article.

Article names are 255 characters in length and do not accept special characters like ,?><|* etc, just like a file name.

As soon as the user presses Enter after typing the article name, the treeview fires AfterLabelEdit event.

Private Sub treeArticles_AfterLabelEdit(sender As Object, e As NodeLabelEditEventArgs) Handles treeArticles.AfterLabelEdit
        ' after the label is edited, add the article name to the database
        If Not (e.Label Is Nothing) Then
            If e.Label.Length > 0 Then
                If e.Label.IndexOfAny(New Char() {"@"c, "."c, ","c, "!"c, "\"c, ":"c, "*"c, "?"c, "<"c, ">"c, "|"c, "/"c}) = -1 Then
                    ' Stop editing without canceling the label change.
                    e.Node.EndEdit(False)
                    ' get new article name and add it to the database
                    articleTitle = e.Label
                    'ensure the article is 255 characters long
                    articleTitle = Strings.Left(articleTitle, 255).Trim
                    articleKey = e.Node.Name
                    articlePrefix = Common.MvField(articleKey, 1, "-")
                    articleID = Common.MvField(articleKey, 2, "-")
                    Select Case articlePrefix
                        Case "new"
                            ' we are adding a new article
                            ' insert a new article to the database
                            Dim article As New Map
                            article.Put("article", articleTitle)
                            SQLite.InsertMap("articles", article)
                            ' get the id of the article from the database
                            articleID = SQLite.RecordReadToMv("articles", "article", articleTitle, "id")
                            PrepareArticle(e.Node, articleID, articleTitle)
                            ' display the article
                            ReadArticle(articleID)
                            Exit Sub
                        Case "article"
                            ' we are updating an existing article, change the article title
                            articleKey = e.Node.Name
                            ' get the article id
                            articleID = Common.MvField(articleKey, 2, "-")
                            ' update a new article to the database
                            Dim orticle As New Map
                            orticle.Put("article", articleTitle)
                            Dim warticle As New Map
                            warticle.Put("ID", articleID)
                            SQLite.UpdateMap("articles", orticle, warticle)
                            ReadArticle(articleID)
                            Exit Sub
                    End Select
                Else
                    ' Cancel the label edit action, inform the user, and 
                    ' place the node in edit mode again. 
                    e.CancelEdit = True
                    MessageBox.Show("Invalid tree node label." & _
                      Microsoft.VisualBasic.ControlChars.Cr & _
                      "The invalid characters are: <a href="mailto:'@'">'@'</a>, '.', ',', '!', '?', '>', '<', '|', '*', ':', '\', '/'", _
                      "Article Edit", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
                    e.Node.BeginEdit()
                    Exit Sub
                End If
            Else
                ' Cancel the label edit action, inform the user, and 
                ' place the node in edit mode again. 
                e.CancelEdit = True
                MessageBox.Show("Invalid tree node label." & _
                  Microsoft.VisualBasic.ControlChars.Cr & _
                  "The label cannot be blank", "Article Edit", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
                e.Node.BeginEdit()
                Exit Sub
            End If
        End If
    End Sub

This checks whether the article name meets the requirements of special characters, makes it 255 characters and then updates the database articles table and creates the folder of the article. The article name for each treeview is prefixed by "article-" and then the article number read from the SQLite table. MvField acts like the split function to get an item from a delimited string. Each new article has an article key/name of new, thus here we check also if we are adding a new article or renaming an existing one. If the article is new, we read the article name and add it to the database, gets its allocated unique autoincrement number, prepare and read the article. Preparing the article ensures that everything about the article is ready.

Sub PrepareArticle(newNode As TreeNode, ID As String, Title As String)
        ' now add the images and links
        Dim zNode As TreeNode = newNode.Nodes.Add("zips-" & ID, "Zip Files", "zip", "zip")
        Dim iNode As TreeNode = newNode.Nodes.Add("images-" & ID, "Media", "camera", "camera")
        Dim lNode As TreeNode = newNode.Nodes.Add("links-" & ID, "Links", "link", "link")
        ' create respective links to project
        articlePath = articlesPath & "\" & ID
        articleBaK = articlePath & "\BAK"
        Files.Dir_Create(articlePath)
        Files.Dir_Create(articleBaK)
        Files.File_CopyToFolder(Common.AppPath & "\quote.gif", articlePath)
        articleFile = articlePath & "\" & ID & ".html"
        ' if the file does not exist, create a blank one
        If Files.File_Exists(articleFile) = False Then
            ' get the blank article contents
            articleContent = Files.File_Data(blankArticle)
            ' save the new blank article
            Files.File_Update(articleFile, articleContent)
        End If
    End Sub

For each article, three sub nodes are created, Zip Files (to hold your source), Media (for all images etc) and Links (for all links created during article creation). An article path is created within your installation folder using the article id e.g. C:\CPS\Articles\1\1.html. This folder will store everything that has to do about an article. A backup folder called BAK is also created that stores all the 1 minute interval versions of your article. This ensures you never ever really loose an article. That's something I wanted to avoid, however due to these saving per 1 minute, you might want to clear these up when done with your article. This method above also checks to see of the article file exists, if not, it copies the contents of the blankArticle i.e. article.txt to the article file.

Managing Files

Reading File Contents

Public Shared Function File_Data(ByVal StrPath As String) As String
        If File.Exists(StrPath) = True Then
            Dim readText As String = File.ReadAllText(StrPath)
            Return readText
        Else
            Return ""
        End If
    End Function

Writing File Contents

Public Shared Function File_Update(ByVal strFileName As String, ByVal strContent As String, Optional ByVal boolAppend As Boolean = False) As Boolean
        Try
            Dim strfolder As String = Files.File_Token(strFileName, FileTokenType.Path, "\", False)
            If Not Files.Dir_Exists(strfolder) Then
                Files.Dir_Create(strfolder)
            End If
            My.Computer.FileSystem.WriteAllText(strFileName, (strContent & ChrW(13) & ChrW(10)), boolAppend)
            Return True
        Catch exc As Exception
            MsgBox(strFileName & vbCr & vbCr & "The file could not be saved! Please try again!", MsgBoxStyle.Critical, "File Save Error")
            Return False
        End Try
    End Function

You can specify a full file path here and it will create a recursive folder for you to store the file. File_Token returns a particular file token, it could be a path, an extension etc.

File Sizes

 Public Shared Function File_SizeName(ByVal Bytes As Long) As String
        If (Bytes >= &H40000000) Then
            Return (Strings.Format((((CDbl(Bytes) / 1024) / 1024) / 1024), "#0.00") & " GB")
        End If
        If (Bytes >= &H100000) Then
            Return (Strings.Format(((CDbl(Bytes) / 1024) / 1024), "#0.00") & " MB")
        End If
        If (Bytes >= &H400) Then
            Return (Strings.Format((CDbl(Bytes) / 1024), "#0.00") & " KB")
        End If
        If ((Bytes > 0) And (Bytes < &H400)) Then
            Return (Conversions.ToString(Conversion.Fix(Bytes)) & " Bytes")
        End If
        Return "0 Bytes"
    End Function

This is for the status bar file size, we read the length of the file and display its size with the various indicators.

Reading the Article

Sub ReadArticle(ID As String)
        Common.HourGlassShow(Me)
        ' create respective links to project
        articlePath = articlesPath & "\" & ID
        articleBaK = articlePath & "\BAK"
        ' define the article path
        articleFile = articlePath & "\" & ID & ".html"
        If Files.File_Exists(articleFile) = True Then
            ' get the file contents
            articleContent = Files.File_Data(articleFile)
            ' load the file to the web browser
            txtContent.DocumentText = articleContent
            ' count the words in the article
            articleWords = CountWords(articleContent)
            ' update the status bar
            StatusMessage(StatusBar, "Words: " & articleWords & " |", 2)
            ' once a document has been loaded, set to design mode
            txtContent.ActiveXInstance.document.designmode = "On"
            ' set the tag, we will use this to save the article
            txtContent.Tag = articleFile
            ' get links for this article
            GetArticleLinks(ID)
            GetArticleMedia(ID)
            'Timer1.Enabled = True
            Dim fsize As Long = Files.Dir_Size(articlePath)
            Dim fsize1 As String = Files.File_SizeName(fsize)
            StatusMessage(StatusBar, "Location: " & txtContent.Tag & " |", 5)
            StatusMessage(StatusBar, "Size: " & fsize1 & "|", 4)
            StatusMessage(StatusBar, "Last Saved: " & Files.File_LastModifiedTime(articleFile) & "|", 3)
            SetHTML(articleContent)
        Else
            SetHTML("")
            StatusMessage(StatusBar, "Words: 0 |", 2)
            StatusMessage(StatusBar, "Location: |", 5)
            StatusMessage(StatusBar, "Last Saved: " & Files.File_LastModifiedTime(articleFile) & "|", 3)
            StatusMessage(StatusBar, "Size: |", 4)
        End If
        Timer1.Enabled = True
        Speech.StopSpeaking()
        HourGlassHide(Me)
    End Sub

This method basically does a couple of things. The article id is read from the node name, e.g. article-1 and returned. The article path is then generated and a check to see if the article file exists on the computer. If it exists, the contents of the file are read and these are displayed within the webbrowser (txtContent). A count of the words in the article is done and then the designMode of the webbrowser turned on to enable webbrowser editing. The path of the article is saved to the webbrowser tag property (this is used to save the article during the intervals).

After that all links that exists in the article are read including the media and these are loaded to the respective nodes per article. The size of the forlder contents is calculated and the status bar updated. If there was any reading of the article text, this is stopped.

Private Sub GetArticleLinks(ID As String)
        Common.HourGlassShow(Me)
        ' find all nodes with this key
        Dim bFound As Boolean = False
        Dim lnkNode As TreeNode = Nothing
        Dim arr As TreeNode() = treeArticles.Nodes.Find("links-" & ID, True)
        For i As Integer = 0 To arr.Length - 1
            lnkNode = arr(i)
            bFound = True
            Exit For
        Next
        If bFound = True Then
            ' remove all listed links
            lnkNode.Nodes.Clear()
            Dim lnkKey As String
            Dim lnkPos As Integer = 0
            Dim eletarget As String
            For Each ele As HtmlElement In txtContent.Document.Links
                lnkPos = lnkPos + 1
                lnkKey = "link-" & ID & "-" & lnkPos
                eletarget = ele.GetAttribute("href")
                lnkNode.Nodes.Add(lnkKey, eletarget, "link", "link")
                Application.DoEvents()
            Next
        End If
        Common.HourGlassHide(Me)
    End Sub

The Links node per article here is cleared and reloaded with any existing links. This happens when a user selects the Links node of the article. An hourglass is shown during the process.

Private Sub GetArticleMedia(ID As String)
        HourGlassShow(Me)
        ' find all nodes with this key
        Dim bFound As Boolean = False
        Dim lnkNode As TreeNode = Nothing
        Dim arr As TreeNode() = treeArticles.Nodes.Find("images-" & ID, True)
        For i As Integer = 0 To arr.Length - 1
            lnkNode = arr(i)
            bFound = True
            Exit For
        Next
        If bFound = True Then
            ' remove all listed links
            lnkNode.Nodes.Clear()
            Dim imgsrc As String
            Dim lnkKey As String
            Dim lnkPos As Integer = 0
            Dim cleanLnk As String
            Dim fName As String
            ' put border on images 
            Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
            For Each image As HTMLImg In doc.images
                If image IsNot Nothing Then
                    imgsrc = image.src
                    imgsrc = imgsrc.Replace("about:", "")
                    lnkPos = lnkPos + 1
                    lnkKey = "media-" & ID & "-" & lnkPos
                    ' copy the images to the article folder
                    If InStr(imgsrc, "<a href="file:///">file:///</a>") > 0 Then
                        cleanLnk = imgsrc.Replace("<a href="file:///">file:///</a>", "")
                        cleanLnk = cleanLnk.Replace("/", "\")
                        articlePath = articlesPath & "\" & ID
                        articleBaK = articlePath & "\BAK"
                        Files.File_CopyToFolder(cleanLnk, articlePath)
                        ' get file name and update media link
                        fName = Files.File_Token(cleanLnk, Files.FileTokenType.FileName)
                        ' try and update image link
                        image.src = fName
                        lnkNode.Nodes.Add(lnkKey, fName, "camera", "camera")
                    Else
                        lnkNode.Nodes.Add(lnkKey, imgsrc, "camera", "camera")
                    End If
                End If
                Application.DoEvents()
            Next
        End If
        HourGlassHide(Me)
    End Sub

The Media node, loads all images, video etc available in the document dom. The image source will be basically a link to the complete file path each time you insert an image via the toolbar. For CodeProject, all the image links should be absolute at the folder level, thus the complete path should be removed. This method, whilst scanning all available links, checks to see if the links are complete file paths and then cleans this up and copies the media file to the article folder.

All this reading of links and media happens when an article is clicked in the treeview, lets look what happens with that below:

Private Sub treeArticles_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles treeArticles.NodeMouseClick
        ' get the selected article
        Dim imgLink As String
        SelectedArticle = e.Node
        If TypeName(SelectedArticle) <> "Nothing" Then
            ' display the tag contents on the html part of the screen
            articleKey = SelectedArticle.Name
            articleID = Common.MvField(articleKey, 2, "-")
            articlePrefix = Common.MvField(articleKey, 1, "-")
            Select Case articlePrefix
                Case "article"
                    ' update title of page
                    Me.Text = "CodeProject.Show: " & SelectedArticle.Text
                    ' read the article
                    ReadArticle(articleID)
                Case "images"
                    Me.GetArticleMedia(articleID)
                    SaveContent()
                Case "links"
                    Me.GetArticleLinks(articleID)
                Case "zips"
                Case "articles"
                    SetHTML("")
                    txtContent.DocumentText = ""
                Case "link"
                    ' open the link in the explorer
                    txtContent.Navigate(SelectedArticle.Text)
                Case "media"
                    ' open the media in the explorer
                    imgLink = articlesPath & "\" & articleID & "\" & SelectedArticle.Text
                    txtContent.Url = New Uri(imgLink)
            End Select
        End If
    End Sub

The selected node is stored in SelectedArticle to reference. The article ID and are read and depending on the prefix:

  • article - read the article details
  • images - load all images/media for the article
  • links - load all links for the article
  • link - open it in the writing area webbrowser control
  • media - open it in the writing are webbrowser control

1.2 File > Delete

This is used to delete your article. You have to select your article from the listed article and click File > Delete to remove it. Deleted articles cannot be undone.

Private Sub cmdDeleteArticle_Click(sender As Object, e As EventArgs) Handles cmdDeleteArticle.Click
        SelectedArticle = treeArticles.SelectedNode
        If TypeName(SelectedArticle) = "Nothing" Then
            Common.MyMsgBox("You need to select an article to delete first!", , , "Delete Article")
        Else
            Timer1.Enabled = False
            articleTitle = SelectedArticle.Text
            articleKey = SelectedArticle.Name
            articleID = Common.MvField(articleKey, 2, "-")
            Dim ans As MsgBoxResult = Common.MyMsgBox("Delete: " & articleTitle & vbCrLf & vbCrLf & _
                                                      "Are you sure that you want to delete this article, you will not be able to undo your changes. Continue?", _
                                                      "yn", "q", "Confirm Delete")
            If ans = MsgBoxResult.No Then
                Timer1.Enabled = True
                Exit Sub
            End If
            Timer1.Enabled = False
            ' continue delete the article from database
            SQLite.RecordDelete("articles", "id", articleID, "integer")
            ' delete the folder
            articlePath = articlesPath & "\" & articleID
            Files.Dir_Delete(articlePath)
            ' delete the node from tree
            SelectedArticle.Remove()
            Timer1.Enabled = True
            ReadArticle(articleID)
        End If
    End Sub

To delete an article, a user needs to select it first. A message box will prompt for confirmation. Once that is done, the article is removed from the SQLite database and the article path cleared and a node that links the article also removed in the treeview.

1.3 File > Rename

Should you want to change your article name, click File > Rename and type over your article name just when you are creating a new article. Only the title of the article will be changed.

Private Sub cmdRenameArticle_Click(sender As Object, e As EventArgs) Handles cmdRenameArticle.Click
        ' user clearing the contents of the article
        SelectedArticle = treeArticles.SelectedNode
        If TypeName(SelectedArticle) = "Nothing" Then
            ' there is no article selected
            Common.MyMsgBox("You have not selected any article to rename yet!", "o", "e")
        Else
            ' set the editing mode to true
            Timer1.Enabled = False
            treeArticles.LabelEdit = True
            ' begin the edit of the node
            If Not SelectedArticle.IsEditing Then
                SelectedArticle.BeginEdit()
            End If
            Timer1.Enabled = True
        End If
    End Sub

A user needs to select an article to rename. When that is done, LabelEdit and BeginEdit as discussed above fired. The article prefix this time is "article-", thus the article will be updated in the database as depicted with AfterLabelEdit above.

1.4. File > Reset

Resetting an article clears all the contents of the article and creates a blank template for you to write on. Only do this when you want to rewrite your article from scratch. This action cannot be undone.

Private Sub cmdResetArticle_Click(sender As Object, e As EventArgs) Handles cmdResetArticle.Click
        ' user clearing the contents of the article
        SelectedArticle = treeArticles.SelectedNode
        If TypeName(SelectedArticle) = "Nothing" Then
            ' there is no article selected
            Common.MyMsgBox("You have not selected any article to reset yet!", "o", "e")
        Else
            Dim ans As MsgBoxResult = Common.MyMsgBox("Reset: " & SelectedArticle.Text & vbCrLf & vbCrLf & _
                                                      "Are you sure that you want to reset this article. All the article contents will be reset. You cannot undo this action. Continue?", "yn", "q")
            Select Case ans
                Case MsgBoxResult.Yes
                    Timer1.Enabled = False
                    ' get the article file
                    articleFile = GetArticleFile()
                    ' get the article id
                    articleID = GetArticleID()
                    ' delete the article file
                    Files.File_Delete(articleFile)
                    ' get the blank article contents
                    articleContent = Files.File_Data(blankArticle)
                    ' write blank article to article
                    Files.File_Update(articleFile, articleContent)
                    ' copy the main.css file to article folder
                    articlePath = articlesPath & "\" & articleID
                    articleBaK = articlePath & "\BAK"
                    Files.Dir_Create(articlePath)
                    Files.Dir_Create(articleBaK)
                    Files.File_CopyToFolder(Common.AppPath & "\quote.gif", articlePath)
                    ' read the new article and display it
                    Timer1.Enabled = True
                    ReadArticle(articleID)
            End Select
        End If
    End Sub

A user will be asked to confirm if they want to reset the article. This deletes the article file and resets the contents of the file with a blank CodeProject submission template.

1.5. File > Save As

This functionality is the same functionality as saving a page from the internet browser. When selected your article is saved as a single htm file. This runs an ExecCommand passing it the filename of the article title.

Private Sub SaveAsToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles SaveAsToolStripMenuItem.Click
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("SaveAs", False, SelectedArticle.Text)
        Timer1.Enabled = True
    End Sub

1.6 File > Close

This exists the application. No confirmation is required from the user.

 Private Sub cmdCloseApp_Click(sender As Object, e As EventArgs) Handles cmdCloseApp.Click
        Application.Exit()
    End Sub

2. Writing and Formatting your article

Now that you have created your new article, it's time to write some content to it. Every time you create a new article, the CodeProject article template is used as a blank article and needs to be updated for your article to have some meat.

Click anywhere in the article and your mouse pointer will start blinking for you to type over. Before writing though you might want to hide the treeview and the HTML view of your article. To do so, click the TreeView Toggle and the HTML Toggle buttons as depicted below.

Private Sub ToolStripButton1_Click_1(sender As Object, e As EventArgs) Handles cmdHideTree.Click
        'toggle the splitcontainer treeview visibility
        Splits()
    End Sub
    Private Sub Splits()
        Dim bStatus As Boolean = SplitContainer1.Panel1Collapsed
        If bStatus = True Then
            SplitContainer1.Panel1Collapsed = False
        Else
            SplitContainer1.Panel1Collapsed = True
        End If
        ' ensure splitters are same width at 50%
        SplitContainer1.SplitterDistance = SplitContainer1.Width / 4
        SplitContainer2.SplitterDistance = SplitContainer2.Width / 2
    End Sub

The HTML toggle is also just next to the treeview toggle. This will open up the whole screen for you to write on. Now lets go on and write our article and format it for publishing.

As you can see from above, the treeview, html controls are placed inside split containers. You hide each panel by running a PanelXCollapsed method within each splitcontainer. I'm also resizing the containers so that when shown together, the web control and html control are 50% each per side.

2.1 Formatting Headers

Most of the controls within the header use the ExecCommands available for the webControl. These are explained in detail here.

The headers button has some functionality to format your headers from H1-H6 and also functionality to remove formatting, insert a <code>, <div> and <address> element to your selected text. The code to format headers works like this. Each header attribute should be enclosed with <>.

Private Sub H5ToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles H5ToolStripMenuItem.Click
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("FormatBlock", False, "<h5>")
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

2.2 Formatting Source Code

You might have source code in your article that you want to format. Select your source code and then click the appropriate format to apply such for the final article for publishing. Your formatted code will be highlighted with orange background as usual and you will see the outcome when you preview your article with the online CodeProject writer as depicted below.

After going through each source code format for each of the programming languages, I also wanted to add that functionality here. Each programming language has its own indicator. These are:

No Language = text
asp.net = aspnet
c# = cs
c++ = C++
c++/cli = mc++
css = css
f# = F#
html = html
java = Java
JavaScript = jscript
masm/asm = asm
msil = msil
midl = midl
php = php
sql = sql
vb.net = vb.net
vbscript = vbscript
xml = xml

FormatBlock could not work for me to ensure the resulting output html meets this, so I wrote a small script.

Private Sub SetProgrammingLanguage(lang As String)
        Timer1.Enabled = False
        ' element to set the attribute
        Dim hElement As IHTMLElement
        ' get the document
        Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
        ' get selected range
        Dim range As IHTMLTxtRange = doc.selection.createRange()
        ' format the code
        range.execCommand("FormatBlock", False, "<pre>")
        ' get the element
        hElement = range.parentElement
        ' add attribute to element
        hElement.setAttribute("lang", lang)
        hElement.removeAttribute("class")
        SaveContent()
        Timer1.Enabled = True
    End Sub

With each button, passing it the programming language concerned. First you select the text that you want to format. A IHTMLtxtRange is created from the selected text, then a formatblock applied to that range with a <pre> element. Then the parent element holding the text is read and a language attribute passed to it with the language concerned. This produces something like this for your selection

<PRE lang=html>contentEditable='true'</PRE>

2.3 Changing the ForeColor of your text

Select the text to apply a forecolor to and click the provided button and choose a color that you want to apply. To apply a ForeColor, we call the FontDialog.

Private Sub cmdChangeColor_Click(sender As Object, e As EventArgs) Handles cmdChangeColor.Click
        Timer1.Enabled = False
        ' open the color selector
        ColorDialog1.SolidColorOnly = True
        ColorDialog1.AllowFullOpen = False
        ColorDialog1.AnyColor = False
        ColorDialog1.FullOpen = False
        ColorDialog1.CustomColors = Nothing
        Dim result As DialogResult = ColorDialog1.ShowDialog()
        If result = Windows.Forms.DialogResult.OK Then SetSelectionForeColor(Me.ColorDialog1.Color)
        Timer1.Enabled = True
    End Sub

and

 Private Sub SetSelectionForeColor(ByVal Color As System.Drawing.Color)
        Timer1.Enabled = False
        ' choose a color for the text
        txtContent.Document.ExecCommand("ForeColor", False, System.Drawing.ColorTranslator.ToHtml(Color))
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

The color dialog enables one to select a color they want and SetSelectionForeColor applies the color to the selectec article text. The color is converted to html format with the ColorTranslartor.

2.4 Changing the Font properties

Also just like the Color dialog, the Font dialog is used to change the font of selected text in an article.

Private Sub cmdFont_Click(sender As Object, e As EventArgs) Handles cmdFont.Click
        Timer1.Enabled = False
        ' open the font selector
        Dim result As DialogResult = FontDialog1.ShowDialog()
        ' assign font to selection
        If result = Windows.Forms.DialogResult.OK Then SetSelectionFont(Me.FontDialog1.Font)
        Timer1.Enabled = True
    End Sub

and

Private Sub SetSelectionFont(ByVal Font As System.Drawing.Font)
        ' set the font for the text
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("FontName", False, Font.Name)
        If Font.Bold And Not txtContent.Document.DomDocument.queryCommandValue("Bold") Then
            txtContent.Document.ExecCommand("Bold", False, Nothing)
        End If
        If Font.Italic And Not txtContent.Document.DomDocument.queryCommandValue("Italic") Then
            txtContent.Document.ExecCommand("Italic", False, Nothing)
        End If
        If Font.Underline And Not txtContent.Document.DomDocument.queryCommandValue("Underline") Then
            txtContent.Document.ExecCommand("Underline", False, Nothing)
        End If
        txtContent.Document.ExecCommand("FontSize", False, ConvertFontSizeToHTMLFontSize(Font.SizeInPoints))
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

The font name is applied to the text and then the bold, italic, underline and fontsize attributes as per properties read from the font dialog.

2.5 Inserting Links

Select the text to apply a link to and click the Link button. Copy or paste the link. To remove a link you need to select it and then click the unlink button. To insert links we call the CreateLink ExecCommand and tell it to show the UI for that function. The variable True in the command.

Private Sub cmdCreateLink_Click(sender As Object, e As EventArgs) Handles cmdCreateLink.Click
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("CreateLink", True, "")
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

2.6 You can also insert a horizontal rule to your content

Private Sub cmdInsertHR_Click(sender As Object, e As EventArgs) Handles cmdInsertHR.Click
        RunCommand("insertHorizontalRule")
    End Sub

2.7 Inserting images

To insert images we also call an ExecCommand and ask it to show its UI.

 Private Sub InsertImageToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles InsertImageToolStripMenuItem.Click
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("InsertImage", True, "")
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

2.8 Inserting Form Controls

To insert form controls, you use the JavaScript portion of the Microsoft Article linked above about ExecCommands. You can find more details here about these controls. As an example, to insert a textbox I have executed.

Private Sub TextToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles TextToolStripMenuItem.Click
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("InsertInputText", False, "txt")
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

2.9 Inserting Anchors / Bookmarks

Inserting anchors / bookmarks calls for one to indicate the bookmark name. I used an inputbox to ask the user for an anchor name.

Private Sub cmdAddAnchor_Click(sender As Object, e As EventArgs) Handles cmdAddAnchor.Click
        Dim bmName As String = InputBox("Please enter the bookmark name below:", "BookMark Name", "BookMark1")
        If Len(bmName) = 0 Then Exit Sub
        Timer1.Enabled = False
        txtContent.Document.ExecCommand("CreateBookmark", False, bmName)
        txtContent.Document.Body.Focus()
        SaveContent()
        Timer1.Enabled = True
    End Sub

3. Print and Preview your Article

These are built in commands within the webbrowser, however the print comman is an ExecCommand.

Private Sub ToolStripButton1_Click(sender As Object, e As EventArgs) Handles cmdPrintPreview.Click
        txtContent.ShowPrintPreviewDialog()
    End Sub
    Private Sub cmdProperties_Click(sender As Object, e As EventArgs) Handles cmdProperties.Click
        txtContent.ShowPropertiesDialog()
    End Sub
    Private Sub cmdPageSetup_Click(sender As Object, e As EventArgs) Handles cmdPageSetup.Click
        txtContent.ShowPageSetupDialog()
    End Sub

4. Article Aloud

For reading article content, I used the speech api. I loaded the available voices to the combo box in the toolbar after initializing the speech class.

Public Shared Function GetVoices() As List(Of String)
        Dim sVoices As New List(Of String)
        Initialize()
        For Each voice As System.Speech.Synthesis.InstalledVoice In synth.GetInstalledVoices()
            sVoices.Add(voice.VoiceInfo.Name)
        Next
        Return sVoices
    End Function

Then to read the text called...

Public Shared Sub StartSpeaking(ByVal strVoice As String, ByVal strText As String)
        synth.Rate = -2
        synth.Volume = 100
        synth.SelectVoice(strVoice)
        synth.SpeakAsync(strText)
        Paused = False
    End Sub

From

Private Sub cmdTextAloud_Click(sender As Object, e As EventArgs) Handles cmdTextAloud.Click
        ' get the voice to use
        Dim strVoice As String = cboVoices.SelectedItem
        If Len(strVoice) = 0 Then
            MyMsgBox("You need to select a voice engine to read the article first!", , , "Speech Engine Error")
            Exit Sub
        End If
        ' read contents of the article out aloud
        Timer1.Enabled = False
        Dim pContent As String = txtContent.Document.Body.Parent.InnerText
        Speech.StartSpeaking(strVoice, pContent)
    End Sub

This detects the selected voice and then reads the text of the article as read from txtContent.Document.Body.Parent.InnerText

5. The TreeView Article Details

When CodeProject.Show starts, the main form is loaded and this code is executed.

Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles Me.Load
        ' run after the form is loaded
        ' ensure the splitters are resized
        ' ensure splitters are same width at 50%
        SplitContainer1.SplitterDistance = SplitContainer1.Width / 4
        SplitContainer2.SplitterDistance = SplitContainer2.Width / 2
        ' create a folder to hold articles
        Files.Dir_Create(articlesPath)
        ' open the database
        SQLite.UseDataFolder = False
        SQLite.Database = "articles.db"
        SQLite.OpenConnection()
        ' load the articles
        RefreshArticles()
        'set up scintilla
        ' set up voices
        Dim mVoices As List(Of String) = Speech.GetVoices
        ' load voices
        CboBoxFromCollection(cboVoices, mVoices, True)
    End Sub

As you can see, a connection to a SQLite database is made called articles.db. This is followed by RefreshArticles which loads all available articles to the tree for selection and also loads available speeches to the combobox voices.

A look at what the SQLite class does is important then. Here we go.

Shared Sub InsertMap(TableName As String, sm As Map)
        Dim sb As New StringBuilder
        sb.Append("INSERT INTO [" & TableName & "] (")
        sb.Append(sm.Columns).Append(") VALUES (").Append(sm.Values).Append(")")
        Dim sCommand As SQLiteCommand = SQLite.OpenCommand(sb.ToString)
        sm.SetSqLiteCommand(sCommand)
        sCommand.ExecuteNonQuery()
    End Sub

This code inserts a new record to a table. We use a dictionary object to define key value pairs from the Map Class. For each field in the map we define it like:

dim m as new Map: m.put("article", "My First Article")

and pass m to this method

To update we need to maps, one for the field values and one for the where clause.

Shared Sub UpdateMap(TableName As String, sm As Map, wm As Map)
        Dim sb As New StringBuilder
        sb.Append("UPDATE [" & TableName & "] SET ")
        sb.Append(sm.ColumnsUpdate).Append(" WHERE ").Append(wm.ColumnsUpdate)
        Dim sCommand As SQLiteCommand = SQLite.OpenCommand(sb.ToString)
        sm.SetSqLiteCommand(sCommand, True)
        wm.SetSqLiteCommand(sCommand, False)
        sCommand.ExecuteNonQuery()
    End Sub

Deleting records also work the same way, we pass it a map field key value pairs of the fiels to delete and call DeleteMap from SQLite.

Shared Sub DeleteMap(TableName As String, wm As Map)
        Dim sb As New StringBuilder
        sb.Append("DELETE FROM [" & TableName & "] WHERE ")
        sb.Append(wm.ColumnsUpdate)
        Dim sCommand As SQLiteCommand = SQLite.OpenCommand(sb.ToString)
        wm.SetSqLiteCommand(sCommand)
        sCommand.ExecuteNonQuery()
    End Sub

The Map Object

This is a dictionary object defined like...

Public Class Map
    Public MapDict As Dictionary(Of Object, Object)
    Public IsInitialized As Boolean = False
    Private Quote As String = Chr(34).ToString

and the Put function just updates the dictionary

Public Sub Put(sKey As Object, sValue As Object)
        ' update the key value pair in the dictionary
        If MapDict.ContainsKey(sKey) = True Then
            MapDict.Item(sKey) = sValue
        Else
            MapDict.Add(sKey, sValue)
        End If
    End Sub
Public Function ColumnsUpdate() As String
        ' define the update statement for the map
        Dim cols As New List(Of String)
        For Each pair As KeyValuePair(Of Object, Object) In MapDict
            Dim sKey As String = pair.Key.ToString
            cols.Add("[" & sKey & "] = @" & sKey)
        Next
        Return String.Join(",", cols)
    End Function

The above method creates part of a sql command to update the record within a table using parameters

6. The HTML Details

The HTML details displayes are done via ICSharpCode.TextEditor. The color coding is just done with one line of code after the content of the control is loaded. The ReadArticle method calls a method called SetHTML, passing it the contents of the article.

Sub SetHTML(articleData As String)
        ' load the text to the text editor
        ' set highlight scheme to html
        txtHTML.Text = articleData
        txtHTML.SetHighlighting("HTML")
    End Sub

To tell the control to mark the text as HTML we just call SetHighlighting. Here is a Nuget Package for that and a CodeProject article on how to use that.

7. The StatusBar

The statusbar shows some interesting statistics about your article. One of those is the Last Saved item. This each time an article is auto saved gets updated. For the timer to work, the article should be selected in the treeview.

 Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        ' save the contents of the document every 1 minute, ensure we are at article level
        Timer1.Enabled = False
        SaveContent()
        Timer1.Enabled = True
    End Sub

SaveContent basically does that. It saves the contents of the article to the Articles folder.

Private Sub SaveContent()
        ' saved content should be an article
        If Len(txtContent.Tag) = 0 Then Exit Sub
        Dim cArticle As TreeNode = treeArticles.SelectedNode
        Dim bakFile As String
        Dim bakDate As String
        If TypeName(cArticle) <> "Nothing" Then
            ' get article key and id
            Dim ak As String = cArticle.Name
            Dim id As String = Common.MvField(ak, 2, "-")
            ak = Common.MvField(ak, 1, "-")
            Select Case ak
                Case "article", "images"
                    'DocumentText does not reflect new changes made after execCommand
                    Dim pContent As String = txtContent.Document.Body.Parent.OuterHtml
                    pContent = pContent.Replace("{{article}}", articleTitle)
                    ' save a backup file just in case ish happens
                    bakDate = DateTime.Now.ToLongDateString.Replace("/", "-").Replace("\", "-").Replace(":", "-")
                    bakDate = bakDate & " " & DateTime.Now.ToLongTimeString.Replace("/", "-").Replace("\", "-").Replace(":", "-")
                    bakFile = articlesPath & "\" & id & "\BAK\" & id & "-" & bakDate & ".html"
                    Call Files.File_Update(bakFile, pContent)
                    ' save to the original file
                    Dim bSaved As Boolean = Files.File_Update(txtContent.Tag, pContent)
                    SetHTML(pContent)
                    Dim fsize As Long = Files.Dir_Size(articlePath)
                    Dim fsize1 As String = Files.File_SizeName(fsize)
                    StatusMessage(StatusBar, "Size: " & fsize1 & " |", 4)
                    ' get last modification date
                    StatusMessage(StatusBar, "Last Saved: " & Files.File_LastModifiedTime(txtContent.Tag) & "|", 3)
            End Select
        End If
    End Sub

This gets the selected treenode from the tree, that should be an article / images. The reason we need this is because the images and links are clickable and will open themselves within the writing area, over-writing your content. Thus we needed to be careful because of the timer firing every 60,000 milliseconds (i.e. 1 second)

The article content is read from the OuterHTML property of the webbrowser control. The file name is cleaned and the artile file updated and also a backup done at the same time to the BAK folder using the time and date of the document when being saved. This also updates the HTML viewer of the new contents. The timer fires even as you type the document.

8. The Article Location & Open In Browser

To open any document with the default file opener, one calls the Process.Start method.

Public Shared Sub File_View(ByVal sFileName As String, Optional ByVal Operation As String = "Open", Optional ByVal WindowState As Microsoft.VisualBasic.AppWinStyle = AppWinStyle.NormalFocus)
        If File_Exists(sFileName) = False Then Exit Sub
        Dim procStart As Process
        Select Case Operation.ToLower
            Case "open"
                procStart = Process.Start(sFileName, WindowState)
                procStart.WaitForExit()
            Case "print"
            Case Else
        End Select
    End Sub

Here we just pass the process the name of the file to open and it will open it with the default application.

To Open the built in Windows File Explorer though we did something else

Public Shared Sub OpenFolder(sFolder As String)
        If Len(sFolder) = 0 Then Exit Sub
        Process.Start("explorer.exe", sFolder)
    End Sub

We called the name of the program we want to start with the folder we want to open.

9. Copying your article to CodeProject

This section will talk about the Publishing section of CodeProject.Show.

Private Sub PublishToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles PublishToolStripMenuItem.Click
        ' user clearing the contents of the article
        SelectedArticle = treeArticles.SelectedNode
        If TypeName(SelectedArticle) = "Nothing" Then
            ' there is no article selected
            Common.MyMsgBox("You have not selected any article to publish yet!", "o", "e")
        Else
            Timer1.Enabled = False
            HourGlassShow(Me)
            articleID = GetArticleID()
            UnsetArticleMedia(articleID)
            SaveContent()
            ' get the content and put in clipboard
            articlePublish = AppPath() & "\publish.txt"
            Files.File_Update(articlePublish, txtContent.Document.Body.InnerHtml)
            HourGlassHide(Me)
            Timer1.Enabled = True
            ' open the new file
            Files.File_View(articlePublish)
        End If
    End Sub

After you have finished writing your article you need to publish it in CodeProject online. Publishing your article extracts all the contents for it to NotePad so that you can copy and paste the HTML to the Source of your article on the web. UnsetArticleMedia cleans up all the image links and remove the full paths from your image links. Then a new publish.txt file is created that will hold your content. The txtContent.Document.Body.InnerHTML holds the HTML content of your document that you can paste to CodeProject "Source"

Points of Interest

This is my second article that I'm writing using CodeProject.Show. I have removed the code of treeArticles_AfterSelect to NodeMouseClick due to a bug.

Quote:
I love CodeProject.Show! @mash

That's all folks. Welcome to the world of offline CodeProject Article writing!!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Anele 'Mashy' Mbanga
Software Developer DanNora Business Solutions
South Africa South Africa
I'm a Bachelor of Commerce graduate, fell inlove with ICT years back with VB5. Used Pick & System Builder to create a windows app. Very curious, developed my first web database app called Project.Show using ExtJS. Published on Google Play Store, learned JQuery Mobile, a project manager at best. My first intranet app eFas with MySQL.

Fear closes people to a lot of things and we hold ourselves back being held by it. Thus the sooner you believe you can't do something, the sooner everything will work towards that belief. Believe in yourself at all times because you can do anything you set your mind to it!

I have a very beautiful woman and four kids, the best joys in the world. East London, South Africa is currently home.

Awards:

Best Mobile Article of February 2015 (First Prize)
http://www.codeproject.com/Articles/880508/Create-a-CRUD-web-app-using-JQuery-Mobile-and-Loca

Best Mobile Article of May 2015 (Second Prize)
http://www.codeproject.com/Articles/991974/Creating-JQuery-Mobile-CRUD-Apps-using-JQM-Show-Ge

Apps
Bible.Show (Android Store App)
https://www.facebook.com/bibleshow
https://play.google.com/store/apps/details?id=com.b4a.BibleShow

JQM.Show (Android Store App)
https://www.facebook.com/jqmshow
https://play.google.com/store/apps/details?id=com.b4a.JQMShow

CodeProject.Show (An offline CodeProject Article writer)
http://www.codeproject.com/Articles/993453/CodeProject-Show-A-CodeProject-offline-article-wri

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.170915.1 | Last Updated 21 May 2015
Article Copyright 2015 by Anele 'Mashy' Mbanga
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid