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)
Dim hElement As IHTMLElement
Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
Dim range As IHTMLTxtRange = doc.selection.createRange()
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
Timer1.Enabled = False
Dim pNode As TreeNode = treeArticles.Nodes("root")
SelectedArticle = pNode.Nodes.Add("new", "New Article", "page", "page")
SelectedArticle.EnsureVisible()
treeArticles.SelectedNode = SelectedArticle
treeArticles.LabelEdit = True
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
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
e.Node.EndEdit(False)
articleTitle = e.Label
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"
Dim article As New Map
article.Put("article", articleTitle)
SQLite.InsertMap("articles", article)
articleID = SQLite.RecordReadToMv("articles", "article", articleTitle, "id")
PrepareArticle(e.Node, articleID, articleTitle)
ReadArticle(articleID)
Exit Sub
Case "article"
articleKey = e.Node.Name
articleID = Common.MvField(articleKey, 2, "-")
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
e.CancelEdit = True
MessageBox.Show("Invalid tree node label." & _
Microsoft.VisualBasic.ControlChars.Cr & _
"The invalid characters are: <a href="mailto:
"Article Edit", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
e.Node.BeginEdit()
Exit Sub
End If
Else
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)
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")
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 Files.File_Exists(articleFile) = False Then
articleContent = Files.File_Data(blankArticle)
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)
articlePath = articlesPath & "\" & ID
articleBaK = articlePath & "\BAK"
articleFile = articlePath & "\" & ID & ".html"
If Files.File_Exists(articleFile) = True Then
articleContent = Files.File_Data(articleFile)
txtContent.DocumentText = articleContent
articleWords = CountWords(articleContent)
StatusMessage(StatusBar, "Words: " & articleWords & " |", 2)
txtContent.ActiveXInstance.document.designmode = "On"
txtContent.Tag = articleFile
GetArticleLinks(ID)
GetArticleMedia(ID)
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)
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
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)
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
lnkNode.Nodes.Clear()
Dim imgsrc As String
Dim lnkKey As String
Dim lnkPos As Integer = 0
Dim cleanLnk As String
Dim fName As String
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
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)
fName = Files.File_Token(cleanLnk, Files.FileTokenType.FileName)
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
Dim imgLink As String
SelectedArticle = e.Node
If TypeName(SelectedArticle) <> "Nothing" Then
articleKey = SelectedArticle.Name
articleID = Common.MvField(articleKey, 2, "-")
articlePrefix = Common.MvField(articleKey, 1, "-")
Select Case articlePrefix
Case "article"
Me.Text = "CodeProject.Show: " & SelectedArticle.Text
ReadArticle(articleID)
Case "images"
Me.GetArticleMedia(articleID)
SaveContent()
Case "links"
Me.GetArticleLinks(articleID)
Case "zips"
Case "articles"
SetHTML("")
txtContent.DocumentText = ""
Case "link"
txtContent.Navigate(SelectedArticle.Text)
Case "media"
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
SQLite.RecordDelete("articles", "id", articleID, "integer")
articlePath = articlesPath & "\" & articleID
Files.Dir_Delete(articlePath)
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
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
Common.MyMsgBox("You have not selected any article to rename yet!", "o", "e")
Else
Timer1.Enabled = False
treeArticles.LabelEdit = True
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
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
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
articleFile = GetArticleFile()
articleID = GetArticleID()
Files.File_Delete(articleFile)
articleContent = Files.File_Data(blankArticle)
Files.File_Update(articleFile, articleContent)
articlePath = articlesPath & "\" & articleID
articleBaK = articlePath & "\BAK"
Files.Dir_Create(articlePath)
Files.Dir_Create(articleBaK)
Files.File_CopyToFolder(Common.AppPath & "\quote.gif", articlePath)
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
Dim hElement As IHTMLElement
Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
Dim range As IHTMLTxtRange = doc.selection.createRange()
range.execCommand("FormatBlock", False, "<pre>")
hElement = range.parentElement
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
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
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
Dim result As DialogResult = FontDialog1.ShowDialog()
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)
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
SplitContainer1.SplitterDistance = SplitContainer1.Width / 4
SplitContainer2.SplitterDistance = SplitContainer2.Width / 2
Files.Dir_Create(articlesPath)
SQLite.UseDataFolder = False
SQLite.Database = "articles.db"
SQLite.OpenConnection()
RefreshArticles()
Dim mVoices As List(Of String) = Speech.GetVoices
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)
If MapDict.ContainsKey(sKey) = True Then
MapDict.Item(sKey) = sValue
Else
MapDict.Add(sKey, sValue)
End If
End Sub
Public Function ColumnsUpdate() As String
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)
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
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()
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
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"
Dim pContent As String = txtContent.Document.Body.Parent.OuterHtml
pContent = pContent.Replace("{{article}}", articleTitle)
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)
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)
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
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
Common.MyMsgBox("You have not selected any article to publish yet!", "o", "e")
Else
Timer1.Enabled = False
HourGlassShow(Me)
articleID = GetArticleID()
UnsetArticleMedia(articleID)
SaveContent()
articlePublish = AppPath() & "\publish.txt"
Files.File_Update(articlePublish, txtContent.Document.Body.InnerHtml)
HourGlassHide(Me)
Timer1.Enabled = True
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!!