Creating a Simple Bug Tracking Application with Voodoo Web by yhi13010

VIEWS: 17 PAGES: 26

									Creating a Simple Bug Tracking Application
with Voodoo Web Controls
By Markus Egger
President, EPS Software Corp.


The Objective
In this whitepaper, I show how to create a simple bug tracking application, called
BugReaper, using Voodoo Web Controls and West Wind Web Connection. Most of the
steps detailed in this whitepaper are valid in other environments as well, such as Active
Server Pages (ASP).

The goal of the BugReaper application is to provide a simple interface for entering and
viewing reported software bugs. Once we enter the bugs using a data entry form, we can
see them in a “knowledge base,” where the application categorizes and displays bugs
using a dynamic HTML tree view. This tree view provides navigation capabilities
without having to make additional, time-consuming trips to the server.
The BugReaper application also features a collapsible sidebar menu that makes it easy to
navigate within the application.

Setting Up the Web Connection Project
As always, the first step is to establish your Web Connection project. For detailed
instructions on this task, refer to the Voodoo Web Controls documentation by clicking
the following link.
     http://www.eps-software.com/knowledgebase/voodoo/_0FT0UXJJA.htm

The BugReaper Main Interface (Homepage)
The general interface for the BugReaper application consists of a title bar that contains
the logo and below that, a sidebar menu to the left and the content area to the right (which
can display additional information):
Generally, every Voodoo page follows the same structure. First, there is something
known as the “Voodoo Page Object.” This object acts as a general container for all other
Voodoo objects. The naming here is somewhat confusing, because there can be more
than one Voodoo page on a Web page. In fact, a Voodoo page is the equivalent of a
<FORM> section in a standard HTML page. However, most sites use single Voodoo
page Web pages.

The Voodoo page also acts as the abstraction platform. Voodoo can run on virtually any
platform, such as Web Connection and ASP. However, the Page object will change in
those scenarios. For instance, in our BugReaper application there is a Web Connection-
specific Page object called “WCPage.”

To create the basic BugReaper interface, we could have many pages that create their own
Page object in the following fashion (this is inside the Web Connection Process class):

FUNCTION HelloWorld
   LOCAL loPage
   loPage = NewObject("WCPage","voodoo.prg")

   loPage.AddObject("Logo","SomeLogoObject")
   loPage.AddObject("Menu","SomeMenuObject")

   loPage.DoEvents()
   loPage.Render()
ENDFUNC
However, in most scenarios it is smarter to subclass the Page object first. This provides
the opportunity to customize it later. This also provides an opportunity to create standard
objects such as the logo and the menu, and to replace them globally when the time comes
to change the look and feel of the application.
Here is the Page object used in BugReaper:

DEFINE CLASS BugPage AS WCPage OF voodoo.prg
   FUNCTION OnLoad
      THIS.ADDOBJECT("mnu","SidebarMenu")
      THIS.mnu.TITLE = "Bug Reaper"
      THIS.mnu.WriteTop([<table><tr><td valign="top">])
      THIS.mnu.WriteBottom([</td><td valign="top">])

      THIS.mnu.AddMenuItem("Homepage","index.bug")
      THIS.mnu.AddMenuItem("Add New Bug","bugform.bug?id=new")
      THIS.mnu.AddMenuItem("Bug List","buglist.bug")
      THIS.mnu.AddMenuItem("Knowledge Base","kb.bug")
   ENDFUNC

   FUNCTION RenderTop
      LOCAL lcHTML
      lcHTML = DODEFAULT()
      lcHTML = lcHTML + [<body topmargin="0" leftmargin="0" ]+;
         [vlink="blue"><img src="bugreaper.gif">]
      RETURN lcHTML
   ENDFUNC

   FUNCTION RenderBottom
      LOCAL lcHTML
      lcHTML = [</td></tr></table>]
      lcHTML = lcHTML + DODEFAULT()
      RETURN lcHTML
   ENDFUNC
ENDDEFINE

In the OnLoad() event of the page, we add our custom menu object. This way, each page
has the same sidebar menu. We also add four default items to the menu: links for
Homepage, Add New Bug, Bug List, and Knowledge Base.

Note: I will discuss the menu object further down below.

Note that we are also adding some plain HTML to the menu object. That HTML wraps
the menu into an HTML table, which is necessary because tables are really the only way
to create a layout on an HTML page (short of advanced DHTML, which would make this
page Internet Explorer specific). The basic idea is to wrap the menu into a table with one
row, and two cells. The first cell contains the menu, while the second cell contains other
content. The red lines show the table border (normally, the border-width is set to zero and
we cannot see the border):
To create this kind of border, we have to render the following tags: <table><tr><td
valign="top"> followed by the menu, followed by </td><td valign="top"> followed
by the page content, followed by </td></tr></table>. The tags required immediately
before and immediately after the menu are created using the WriteTop() and
WriteBottom() methods of the menu.
The question remaining is, “where do we actually end the table?” The answer is “in the
Render events.” Let me explain in more detail:

Whenever Voodoo renders an object (“turns it into HTML”), it does so by calling the
Render() method. This method returns the HTML string that represents the object.
Internally, the Render() method calls RenderTop() and RenderBottom(). RenderTop()
fires first, and returns a whole set of HTML to the Render me thod. Then, the Render()
method renders all the member objects (such as the menu inside the page), and finally
RenderBottom() is called, which can create some additional output.

As an example, consider a page with a pageframe, two pages, and a button in the first
page. Events occur in the following order:
The page generally does not generate much output on RenderTop(). The pageframe
creates the page headers in RenderTop(), if enabled. The page itself starts a table cell
with a border in RenderTop(). The button creates an <INPUT…> tag in RenderTop() and
does not create anything in RenderBottom(). Then, the page closes the cell in its
RenderBottom() event. The pageframe itself does not do anything in RenderBottom(). If
page 1 was visible, the second page does not render much in RenderTop() or
RenderBottom() nor do the pageframe or the actual page.

This is somewhat simplified, but it gives you an overview of some of Voodoo’s inner
workings.

In our custom page class, we take advantage of some of these events. We override the
RenderBottom() method of the page to create HTML that closes the table we have
opened on the menu. In addition, we return the default result set created by a page. In the
overview above I told you the page does not render much in its RenderBottom() event.
Well, this was somewhat simplified. So make sure you do not forget about DoDefault()!
Note: Although HTML is somewhat forgiving in rendering content (and whether the
underlying HTML is well formed… in other words, the HTML has as many open tags as
close tags in the reverse order), mismatched table constructs can turn into terribly
messed up interfaces that are hard to debug. My recommendation is to start out simple
and test each piece individually.

At this point, the only thing that is missing is the logo. To add the logo, we simply add an
image tag to the RenderTop() of the page. Of course, I could have also added a
WebImage object to my page, but this was easy enough to do, and adding objects is more
costly in terms of performance than it is to add a few characters to the output stream.

We are now ready to use our new page class in one of Web Connection’s process
methods:

FUNCTION INDEX
   LOCAL loPage
   loPage = CREATEOBJECT("BugPage")

   loPage.ADDOBJECT("h1","WebHeading")
   loPage.h1.CAPTION = "Welcome to BugReaper.com!"

   loPage.DOEVENTS()
   Response.WRITE(loPage.Render())
ENDFUNC

This represents the homepage of our application. Note that I added a WebHeading object
to display a welcome message. This is a custom object that we will create later in this
whitepaper.

Adding a New Bug
To add a new bug, we can simply navigate to the bug entry form (BugForm.bug) and pass
“new” as the ID parameter. Depending on how you configured your system, the URL
may vary, but it should be similar to the following:

http://localhost/bugreaper/bugform.bug?id=new

Of course, we do not make the user type in this URL, but we will automatically navigate
there through the menu (see below for more information on the menu). Either way, you
end up with a data entry form similar to the following:
Since we are using a Web Connection application, this form is instantiated in a method
named BugForm. Here is the code for that method:

   FUNCTION BugForm
      LOCAL loPage
      loPage = CREATEOBJECT("BugPage")

       loPage.ADDOBJECT("frm1","BugEdit")

      loPage.DOEVENTS()
      Response.WRITE(loPage.Render())
   ENDFUNC

This method simply creates an instance of our standard page. As we have already seen,
this includes the menu and the logo. The method then adds an object named “frm1”,
which is an instance of the BugEdit class. Here is the code for that class:

DEFINE CLASS BugEdit AS WebDataForm OF voodoo.prg
CAPTION = "Edit Bug"
CancelUrl = "buglist.bug"
ToolbarClass = "BugFormToolbar"
ToolbarClassLib = ""

LUPDATE = .F.


FUNCTION OnLoad
   DODEFAULT()

   SELECT 0
   USE "c:\wconnect\bugs.dbf"
   LOCATE FOR ID = THIS.PAGE.REQUEST.QueryString("id")
   SET MULTILOCKS ON
   CURSORSETPROP("Buffering",4)
   IF NOT FOUND()
      APPEND BLANK
      REPLACE DateEntered WITH DATE()
      REPLACE ID WITH SYS(2015)
   ENDIF

   THIS.ADDOBJECT("txtID","BugTextbox")
   THIS.txtID.FirstTableRow = .T.
   THIS.txtID.LABEL = "ID"
   THIS.txtID.WIDTH = 70
   THIS.txtID.CONTROLSOURCE = "bugs.id"
   THIS.txtID.READONLY = .T.

   THIS.ADDOBJECT("txtTitle","BugTextbox")
   THIS.txtTitle.LABEL = "Title"
   THIS.txtTitle.WIDTH = 400
   THIS.txtTitle.CONTROLSOURCE = "bugs.title"

   THIS.NEWOBJECT("txtDate","WebDropDownCalendar",;
     "voodoo/voodoo2.prg")
   THIS.txtDate.LABEL = "Date"
   THIS.txtDate.AddTableRow = .T.
   THIS.txtDate.CONTROLSOURCE = "bugs.dateentered"

   THIS.ADDOBJECT("txtRepo","BugEditbox")
   THIS.txtRepo.LABEL = "Repro Scenario:"
   THIS.txtRepo.CONTROLSOURCE = "bugs.repo"

   THIS.ADDOBJECT("txtResults","BugEditbox")
   THIS.txtResults.LABEL = "What Happened?"
   THIS.txtResults.CONTROLSOURCE = "bugs.results"

   THIS.ADDOBJECT("txtExpected","BugEditbox")
   THIS.txtExpected.LABEL = "Expected Results:"
   THIS.txtExpected.CONTROLSOURCE = "bugs.expected"

   THIS.ADDOBJECT("txtCat","WebCombobox")
   THIS.txtCat.ADDLISTITEM("Crash and Burn")
   THIS.txtCat.ADDLISTITEM("Bogus")
   THIS.txtCat.ADDLISTITEM("Operator Error")
   THIS.txtCat.ADDLISTITEM("Stupidity")
       THIS.txtCat.ADDLISTITEM("OS Error")
       THIS.txtCat.AddTableRow = .T.
       THIS.txtCat.LABEL = "Category"
       THIS.txtCat.WIDTH = 200
       THIS.txtCat.CONTROLSOURCE = "bugs.category"

      THIS.ADDOBJECT("txtSev","WebCombobox")
      THIS.txtSev.ADDLISTITEM("1 - Low","1")
      THIS.txtSev.ADDLISTITEM("2 - Still Low","2")
      THIS.txtSev.ADDLISTITEM("3 - Getting worse","3")
      THIS.txtSev.ADDLISTITEM("4 - Pretty bad","4")
      THIS.txtSev.ADDLISTITEM("5 - Really really bad!","5")
      THIS.txtSev.LABEL = "Severity"
      THIS.txtSev.WIDTH = 150
      THIS.txtSev.AddTableRow = .T.
      THIS.txtSev.LastTableRow = .T.
      THIS.txtSev.CONTROLSOURCE = "bugs.Sev"
   ENDFUNC

   FUNCTION Render
      LOCAL lcHTML
      lcHTML = DODEFAULT()
      IF NOT THIS.LUPDATE
         TABLEREVERT(.T.)
      ELSE
         TABLEUPDATE(.T.)
      ENDIF
      USE IN Bugs
      RETURN lcHTML
   ENDFUNC

   FUNCTION SAVE
      IF EMPTY(THIS.txtTitle.VALUE)
          THIS.txtTitle.LabelColor = "red"
          THIS.StatusText = [<font color="red">Please provide a] +;
            [ title!</font>]
          RETURN
      ENDIF
      THIS.LUPDATE = .T.
   ENDFUNC
ENDDEFINE

As you can probably tell, this class comes from the WebDataForm class, which is
Voodoo’s standard data entry form class. This class provides the basic look and feel of
the HTML form.

We default some of the properties to non-standard values. For instance, we set the caption
of the form to “Edit Bug.” We also set the CancelURL to “BugList.bug.” We load this
URL if the user clicks the “Cancel” button.

By default, a WebDataForm has several toolbar buttons, such as “Save,” “Save and
Close,” “Cancel,” and navigation buttons. However, in our case we do not want
navigation buttons. On the other hand, we want a custom button to add new bugs right
from within this form. To accomplish this, we have to use a non-standard toolbar. We
specify that by setting the ToolbarClass and ToolbarClassLib properties to reference our
new toolbar instead of the standard one. Notice that I set the ToolbarClassLib property to
blank (""), since the class resides in the current program (PRG). Otherwise, I would use
this property to indicate where the class is defined.
Here is the code for our non-standard toolbar:

DEFINE CLASS BugFormToolbar AS WebDataFormToolbar
   ShowNavigationButtons = .F.

   FUNCTION OnLoad
      DODEFAULT()

      THIS.ADDOBJECT("cmdNew","NewButton")
      THIS.cmdNew.Raised = .F.
      THIS.cmdNew.BeforeClickConfirmation =;
          "Do you really want to create a new bug and discard the "+;
          "current information?"
   ENDFUNC
ENDDEFINE

We set the ShowNavigationButtons property to False (.F.), which hides the navigation
buttons. In addition, we add another button to the toolbar using the OnLoad() event. Note
that before we add our own button, we execute the default behavior (DoDefault()). This
creates the standard buttons. Afterwards, our own button is added. Note that we could
have done the opposite and added our button before we called DoDefault(), which would
have added our button to the left (before) the default buttons.

Toolbar buttons do not normally have a border, unless the user moves the mouse over
them. All other buttons usually have a border. Since we are adding a standard button to
the toolbar, we need to turn the border off. We do so using the Raised property.

Note that whenever the user clicks the New button, all current information is lost. That is
a bad thing, and we want to ensure that its’ what the user intended to do. For this purpose,
we want to show a message box to allow the user to confirm the action. We can simply
do so by setting the BeforeClickConfirmation property. This launches a message box with
OK and Cancel buttons. If the user hits Cancel, it is as if the user never clicked the button
and the click event never happens on the server..

The New button itself is rather straightforward. It is a simple subclass of WebFlatButton:

DEFINE CLASS NewButton AS WebFlatButton OF voodoo.prg
   CAPTION = "New Bug"
   Image = "SmallBug.gif"
   Width = 90

   FUNCTION CLICK()
      THIS.PAGE.Navigate("bugform.bug?id=new")
   ENDFUNC
ENDDEFINE
As you can see, the Click() event simply causes a navigation to the same data entry form.

The Form’s OnLoad() Event
Let us proceed with the actual form. Most of the work is done in the OnLoad() event.
First, we open a Visual FoxPro table (DBF) that contains the bug information (see below
for the detailed table structure):

SELECT 0
USE "c:\wconnect\bugs.dbf"
LOCATE FOR ID = THIS.PAGE.REQUEST.QueryString("id")
SET MULTILOCKS ON
CURSORSETPROP("Buffering",4)
IF NOT FOUND()
   APPEND BLANK
   REPLACE DateEntered WITH DATE()
   REPLACE ID WITH SYS(2015)
ENDIF

Note that we use the Request object’s QueryString() method to retrieve the ID parameter.
We then try to locate that parameter as the ID in our bugs table. If we cannot find the ID
(as it would be the case if the parameter was “new,” since this is not a valid ID in this
scenario), we add another record to the table.

At this point, things get interesting. In Web-based scenarios, you cannot perform
pessimistic locks, where a record is locked as a user edits the record, because we have a
disconnected environment. So if we were just to add a record every time a user navigates
to this form, we might end up with a whole bunch of empty records, because we cannot
guarantee that the user will click Save or Cancel. In fact, the user might just close the
browser window, and we would never know about it.

For this reason, we turn table buffering on before we add the record. Once the page has
rendered, we immediately issue a TableRevert(), which deletes the newly created record.
In fact, the record never really was created, because due to the table buffering, we really
only operated of a temporary copy of our table.
Notice that the TableRevert() occurs in the render method. We will investigate this in
detail a bit later.

Next, we add all the controls to the form. We do so with simple AddObject() and
NewObject() method calls. We set a number of properties on each object, such as the
width of the control and the control label, (most Voodoo objects enable you to specify a
label property instead of adding a label object, which is more efficient in execution). We
also specify the layout of the form right in this code. It is important to highlight that there
is a major difference in creating form layout between Windows® applications and Web
applications.

In Windows applications, you can absolutely position controls by specifying top and left
properties as well as moving controls around on the form visually. In Web applications,
on the other hand, layouts are generally sequential, with one control positioned right after
the other. Layout information is added through HTML tags such as <BR>. You can
achieve more advanced layout by using borderless HTML tables. (You can also
absolutely position controls on Web pages using DHTML and styles. However, this only
works in Internet Explorer and you lose the ability to resize fonts, and the like.)
Voodoo can help a developer create table-based layout on a form. In fact, most Voodoo
controls feature an AddTableRow property. If this property is set to True (.T.), Voodoo
automatically adds a table row in the following fashion:

<TR><TD>Label</TD><TD>Control</TD></TR>

If there are multiple controls on a form, Voodoo builds a construct like so:

<TR><TD>Label</TD><TD>Control</TD></TR>
<TR><TD>Label</TD><TD>Control</TD></TR>
<TR><TD>Label</TD><TD>Control</TD></TR>

If you are familiar with HTML table syntax, you will probably notice that this is valid
HTML table syntax, but it is not complete. We need to add a <TABLE> and </TABLE>
tag in the following fashion:

<TABLE>
<TR><TD>Label</TD><TD>Control</TD></TR>
<TR><TD>Label</TD><TD>Control</TD></TR>
<TR><TD>Label</TD><TD>Control</TD></TR>
</TABLE>

We can do so, by setting the FirstTableRow and LastTableRow properties to True (.T.) on
the first and last control in our block of controls. If you examine the code above, you will
see that this is exactly what we do to create our form layout. Note that we are not setting
the AddTableRow property to True (.T.) on every control. That is because some of our
classes, such as BugTextbox, have this property set to True (.T.) by default.

If you run the page in debug mode (set DebugOutput to True (.T.) on the page object),
Voodoo renders a table border, which enables you to see how the table renders to create
the layout:
Note: This form contains a WebDropDownCalendar control, which is part of the Voodoo
extended package, not the base package.

The only thing left unexplained in the OnLoad() event is the data binding. Voodoo
features advanced, bi-directional data binding. By definition, data binding should be bi-
directional, which means that data displayed in a form is bound back to the database once
it has been updated. However, many other Web environments support display-only data
binding, which is why we call our data binding “bi-directional”. Setting up the data
binding is almost trivial. As with any Visual FoxPro Windows form, the ControlSource
property enables you to specify the table and field name with which to bind.

Running the Form
Now let us investigate how the form actually runs. Whenever a user navigates to the form
to create a new bug, our code executes and the OnLoad() event fires. This creates the
form with all its controls. It also creates a new record in the database. The new
information displays on the form. Then, the form’s Render() method fires, to turn the
form into HTML. Immediately after the form renders, we check the lUpdated property. If
this property is False (.F.) (the default), we then immediately issue a TableRevert() to
make sure the new record does not end up in the database quite yet. This is important! If
the user was to simply navigate away from this form, or perhaps even close the browser,
we wouldn’t have a way to delete the empty record because the server has no indication
whatsoever that the user disappeared.

At this point, the form renders and the user can interact with it. Now let us suppose the
user clicks “Save.” This re-creates our form on the server, which includes running our
OnLoad() code again. This also re-creates a new record in the database. This in fact re-
creates the entire environment the way we left it before. In fact, to the developer, there is
no difference between keeping the environment alive, and re-creating it. You might now
express concerns about field values that may have been set. Do not worry: Voodoo takes
care of these details for you. In fact, Voodoo re-creates the entire environment just the
way it was originally, and for each individual user that hits the server. In addition,
Voodoo does so without preserving any state on the server, making it easy to scale
Voodoo applications across server clusters.

Then, the Save() event is fired on the form. Here is the code that goes along wit h that:

   FUNCTION SAVE
      IF EMPTY(THIS.txtTitle.VALUE)
         THIS.txtTitle.LabelColor = "red"
         THIS.StatusText = [<font color="red">Please provide a ]+;
            [title!</font>]
         RETURN
      ENDIF
      THIS.LUPDATE = .T.
   ENDFUNC

As you can see, the code first checks to make sure the user provided a title (you could
add more business logic here, or perhaps call out to a business object to verify the input).
If the title field is empty, the code highlights that field by turning the label color to red
and by displaying an error message in the status bar:
Note the status bar is just an HTML string, which can be as simple as plain text, or as
complex as any HTML page. In this example, we simply added some tags to turn the
color red.

Of course, since we violated our “sophisticated” business rule (title cannot be blank) in
this scenario, we do not intend to save the record. For this reason, we simply RETURN
out of the Save() event. The next thing that happens is the Render() me thod. In addition,
just like before, the Render() method looks at the lUpdated property immediately after
rendering the form. Since this property is still False (.F.), a TableRevert() is issued again,
wiping out the new, blank record created.

Now let us assume the user actually provides a title and our simple business rule is
satisfied. Again, the server is hit, the OnLoad() fires, and a blank record is created. Then,
the Save() event sets the lUpdate property to True (.T.). That is really all the Save()
method has to do. Subsequently, the Render() method executes and determines that the
property value is True (.T.). This in turn triggers a Table Update() and, finally, our record
is saved to the database. Note that we do not have to worry about the field values getting
stored into the database. As mentioned above, Voodoo supports bi-directional data
binding and therefore binds new values back into new records automatically.

Now lets suppose the user hit “Cancel” instead of “Save.” In that case, lUpdated never
gets set to True (.T.), so all changes in the database are automatically reverted before the
system navigates to the CancelURL we provided.

At this point, you might think, “gee, this is pretty complicated.” However, it really is not.
We have taken care of all of this for you. I simply explain this in detail so you get a good
understanding of how this works internally and why you do not have to worry about it.
All you have to do is put a TableRevert() and TableUpdate() into the Render() method,
and add business rules to the Save() event.

Displaying and Editing an Existing Bug
At this point, you know everything that goes into displaying an existing bug. The only
difference is that instead of passing in “new” as the parameter, we pass in the record
primary key (PK). This means that the LOCATE in the OnLoad() event finds a record
and navigates to it instead of creating a new one. TableRevert() and TableUpdate() in the
Render() method determine whether changes make it into the database.

The Bug List
The Bug List displays a list of current bugs in the system. Here is a screen shot:




This list contains all of the existing bugs and displays the most important information
about those issues. There are several things to point out here. First, notice that the table
uses alternate back colors to enhance readability. Secondly, there are “Previous” and
“Next” links at the bottom of the listing. This enables you to display only a certain
number of bugs at once.

So let us see how Voodoo creates this list. Here is the code in the process method that
creates the page:

   FUNCTION BugList
      LOCAL loPage
      loPage = CREATEOBJECT("BugPage")

       loPage.ADDOBJECT("h1","WebHeading")
       loPage.h1.CAPTION = "Current Bugs!"

       loPage.ADDOBJECT("cmdNew","NewButton")
       loPage.cmdNew.WRITE([<br><br>])

       loPage.ADDOBJECT("tab1","BugList")

      loPage.DOEVENTS()
      Response.WRITE(loPage.Render())
   ENDFUNC

After creating our standard page (to get the menu and the logo), we add a Web heading to
indicate for the user the kind of information displayed (see below for more information
on the custom WebHeading class). Secondly, we add the same NewButton class that we
used on the data entry form. This button simply navigates to the entry form with “new” as
the parameter. Finally, we add a class named “BugList.” This is where all the magic
happens. Let us have a look at that class:

DEFINE CLASS BugList AS WebTable OF voodoo.prg
   ROWSOURCE = "bugs"
   PageItems = 20
   FireCellRenderEvents = .T.
   HighlightExpression = "sev = 5"
   HighlightBackColor = "red"
   AlternateBackColor = "MOCCASIN"

   FUNCTION Render
      SELECT 0
      USE "c:\wconnect\bugs.dbf"
      LOCAL lcHTML
      lcHTML = DODEFAULT()
      USE IN Bugs
      RETURN lcHTML
   ENDFUNC


   FUNCTION RenderHeader( nHeader, lCancelOriginalHeader )
      lCancelOriginalHeader = .T.
      DO CASE
         CASE nHeader = 1
            RETURN "<font color=red>Bug #</font>"
         CASE nHeader = 3
            RETURN "Date"
         CASE nHeader = 4
            RETURN "Scenario"
         OTHERWISE
            lCancelOriginalHeader = .F.
            RETURN ""
      ENDCASE
   ENDFUNC

   FUNCTION BeforeRenderCell( nRow, nCell, lCancel)
      RETURN [<a href="bugform.bug?id=]+Bugs.ID+[">]
   ENDFUNC
   FUNCTION AfterRenderCell( nRow, nCell)
      RETURN [</a>]
   ENDFUNC
ENDDEFINE

First, we set the RowSource property to “bugs,” since this is the table on which to base
the grid. This requires that the BUGS.DBF table be in use. We take care of that in the
Render() method. We open the table just before the control renders and close the table
right afterwards.

Note that this control always renders all of the fields in a table. To limit the number of
displayed fields, narrow the field list down using a SELECT command. You can also use
a SELECT command to narrow the scope (number of rows). You could achieve the same
using a SET FILTER TO command.

We can set the background colors of the table rows using the BackColor and
AlternateBackColor properties.

By default, the WebTable renders the field name as the column header. However, there
are ways to change this. The WebTable fires events before rendering each header and
each cell. Voodoo disables these events by default to provide maximum performance.
However, we can enable these events by setting the FireCellRenderEvents property to
True (.T.). This causes the RenderHeader() as well as the BeforeRenderCell() and
AfterRenderCell() events to fire.

The RenderHeader() method fires just before each column header is rendered. There are
two parameters passed to this event: the index (sequential number) of the rendered cell,
and a cancel parameter (I will explain that in a moment). We are using a simple CASE
statement to return different HTML depending on the rendered cell number. Note that the
return HTML can contain HTML tags. That is how we turn the first heading red.

Normally, whatever returns from this event is added to the cell with the default text,
which is the field name. However, you can set the lCancelOriginalHeader property to
True (.T.) to prevent the original header text from rendering. This completely replaces the
original caption. Note that we only replace the caption of column 1, 3 and 4. Otherwise,
we return an empty string and specify that the original caption is not removed.

The BeforeRenderCell() and AfterRenderCell() events behave very similar. The main
difference is that the parameters include a row number as well as a cell number. In our
example, we stick to all original rendering behavior, except, we add a hyperlink to the
edit form (with the bug ID as the parameter). We do that for every single field in the
table. Note that we could easily limit this to individual columns using the nCell
parameter. Note that we start the hyperlink in the BeforeRenderCell() event. Then, the
original content (field value) is rendered and, finally, we end the hyperlink in the
AfterRenderCell() event.
Again, we could use the nCancel parameter to prevent the original cell content from
rendering. This parameter is only available in the BeforeRenderCell() event, which makes
sense, because by the time the AfterRenderCell() event fires, the cell content has already
rendered, and it would be too late to cancel that anyway.

As mentioned above, Voodoo enables you to limit the number of displayed records and to
flip back and forth between “pages” of data using the “Next” and “Previous” links. All
you have to do to activate this behavior is to set the PageItems property to the number of
desired items per page:

   PageItems = 20

The WebTable class also has the ability to highlight certain rows. For instance, you may
want to highlight all bugs with a severity level of “5”:




We can achieve this by setting the HighlightExpression to any valid Visual FoxPro
expression and by setting the HighlightBackColor to the desired color:

   HighlightExpression = "sev = 5"
   HighlightBackColor = "red"

Note: The WebTable control is a relatively simple but efficient control. The WebGrid
object, which is part of the Extended Package, is a more sophisticated grid control that
allows great control over the displayed columns and the controls displayed within each
cell. It is possible to embed other Voodoo controls and even forms in each cell, for
instance.

The Knowledge Base
The knowledge base is simple in its concept, but very sophisticated considering we are
using an HTML interface. We want to use a tree view that the user can expand and
collapse to drill down into different categories of bugs. We want this to work quickly
without making slow trip s to the server every time the user expands or collapses a node:
To implement this page, we will use a control that ships in the Voodoo Extended
Package, called the WebDHTMLTree control.

As always, the process class has a method that implements a standard page including the
logo and the menu:

   FUNCTION KB
      LOCAL loPage
      loPage = CREATEOBJECT("BugPage")

       loPage.ADDOBJECT("h1","WebHeading")
       loPage.h1.CAPTION = "Known Bugs!"

       loPage.NEWOBJECT("t1","WebDHTMLTree"," voodoo2.prg")
       SELECT 0
       USE "c:\wconnect\bugs.dbf"

       SELECT Category FROM Bugs INTO CURSOR TCategory GROUP BY 1
       LOCAL loNode, loNode2

       SCAN
          loNode = loPage.t1.AddNode(TCategory.Category,"folder.gif")
          SELECT Bugs
          SCAN FOR Category = TCategory.Category
             IF Bugs.sev = 5
                loNode2 = loNode.AddNode(;
                   [<font color="red">]+Bugs.TITLE+;
                   [</font>],"document.gif")
             ELSE
                loNode2 = loNode.AddNode(Bugs.TITLE,"document.gif")
             ENDIF
             loNode2.URL = "bugform.bug?id="+Bugs.ID
          ENDSCAN
       ENDSCAN
      USE IN Bugs
      loPage.DOEVENTS()
      Response.WRITE(loPage.Render())
   ENDFUNC

What is interesting here is that although the interface is very sophisticated (considering
we use a Web browser), the code is almost trivial and definitely easier than doing this in
Windows.

First, we add a WebDHTMLTree control to the page. Then, we open the bugs table and
query all the categories contained within it. We then iterate over all the categories and
add one node for each category to the tree control, using the AddNode() method. We
simply pass the display text and the node image as parameters.

We then proceed to iterate over the actual bugs for each category. Again, we call the
AddNode() method to add nodes to the tree, except this time we call the AddNode() of
the node we just created, hence creating a hierarchy.

Note that this tree is very flexible. Each node can contain any HTML. In our example, we
stick to simple text and a link (to the data entry form), but the HTML could also be very
complex and even represent entire forms. The same is true for the image. There are no
size restrictions or anything like that. Not only is it easier to create this tree with Voodoo
than with Windows, but the Voodoo tree is also much more powerful and flexible.
Consider the following example for instance:




In this example, we add more HTML to each node to display a question and a dropdown
list. Here is the code:
loNode2 = loNode.AddNode(Bugs.TITLE+;
   [<p align="center">Is this bug important to you?<br>]+;
   [<select><option>Yes<option>No</select></center>],"document.gif")

Each node now has this little survey attached. Note that the expanding and collapsing
functionality is still operational.

The Web Heading Control
At this point, you have a solid understanding of how this sample application works.
However, there are a few non-standard controls in this application to demonstrate how
easy it is to create Voodoo Custom Controls. One of them is the WebHeading control,
which we use on almost every page.

The basic idea of Voodoo Controls is simple: Each control has a Render() method that
returns HTML representing the control. A button for instance, will return HTML that
renders a button on the client. Most controls use a RenderTop() and RenderBottom()
method to render individual parts and to support contained member controls (see diagram
towards the beginning of this whitepaper).

The idea of the WebHeading control is simple. It will simply render a heading tag such as
the following:

<H1>Caption</H1>

Here is the code that does that:

DEFINE CLASS WebHeading AS Web Control OF voodoo\voodoo.prg
      Caption = "WebHeading1"
      Level = 1

      FUNCTION RenderTop
            LOCAL lcHTML
            lcHTML = [<h]+TRANSFORM(THIS.Level)+;
               [>]+THIS.Caption+[</h]+TRANSFORM(THIS.Level)+[>]
            RETURN lcHTML
      ENDFUNC
ENDDEFINE

The control has two properties: Level and Caption. The RenderTop() method uses these
controls to construct the HTML output. If we were to set the Level property to “3” and
the Caption to “Hello World,” the following output would be the result:

<h3>Hello World</h3>

You now know the secret behind the Voodoo Magic!

The SidebarMenu Class
The SidebarMenu class follows the same idea, but it is a little more complex. Here is the
code:
DEFINE CLASS SidebarMenu AS WebControl OF voodoo\voodoo.prg
   TITLE = "Sidebar Menu"
   BORDERCOLOR = "#0067FF"
   FontColor = "white"
   Expanded = .T.
   cItems = ""
   ContentWidth = 200

   FUNCTION AddMenuItem(lcItem,lcLink)
      THIS.cItems = THIS.cItems + ;
         [<li><a href="]+lcLink+[">]+;
         lcItem+[</a>]
   ENDFUNC

   FUNCTION INIT
      THIS.PreserveProperty("Expanded")
      RETURN DODEFAULT()
   ENDFUNC

   FUNCTION RenderTop
      LOCAL lcHTML
      lcHTML = [<table cellspacing="0" cellpadding="0"><tr>]
      IF THIS.Expanded
         lcHTML = lcHTML + [<td bgcolor="]+THIS.BORDERCOLOR+;
            [" width="]+TRANSFORM(THIS.ContentWidth)+[">]
         lcHTML = lcHTML + [<font face="Verdana" size="-1" ]+;
            [color="]+THIS.FontColor+["><b>&nbsp; ]+THIS.TITLE
         lcHTML = lcHTML + [</td>]
      ENDIF
      lcHTML = lcHTML + [<td>]

      * We create an event script that we will then link to the
      * OnClick event of the image. This will fire the toggle()
      * event on the current control
      LOCAL lcName
      lcName = THIS.CreateEventScript("toggle")

      IF THIS.Expanded
         lcHTML = lcHTML + [<img src="collapsebutton.gif" ]+;
            [alt="Collapse" ]+;
            [onclick="]+lcName+[()">]
      ELSE
         lcHTML = lcHTML + [<img src="expandbutton.gif" ]+;
            [alt="Expand" ]+;
            [onclick="]+lcName+[()">]
      ENDIF
      lcHTML = lcHTML + [</td></tr>]
      lcHTML = lcHTML + [<tr>]
      IF THIS.Expanded
         lcHTML = lcHTML + [<td valign="top">]+;
            [<font face="verdana" size="-1"><br>]
         lcHTML = lcHTML + THIS.cItems
      ENDIF
      RETURN lcHTML
   ENDFUNC
   FUNCTION RenderBottom
      LOCAL lcHTML
      lcHTML = ""
      IF THIS.Expanded
         lcHTML = [</td>]
      ENDIF
      lcHTML = lcHTML + [<td><img
src="menuside.gif"></td></tr></table>]
      RETURN lcHTML
   ENDFUNC

   FUNCTION Toggle
      THIS.Expanded = !THIS.Expanded
   ENDFUNC
ENDDEFINE

As mentioned above, the best way to create layout in HTML is tables, which is what we
do in this example. First, we build a table with a blue top row (or whatever the border
color property defines). The RenderTop() method creates everything up to the content
area of the menu (including the content). The actual content (the “menu items”) are
stored in a cItems property. We will investigate how they get there in just a moment.

The RenderBottom() method closes the table by adding a second column, which displays
the “Quick Access Menu” text (which is actually an image). Note that we can expand and
collapse the menu:
The idea is simple: The menu really consists of two columns. The first column shows the
content and the second column shows the blue, vertical border (and graphic). If we
collapse the menu, only the second column renders. We can determine whether to render
the menu collapsed by examining the Expanded property. It is set by the Toggle() event,
which fires whenever you click on the chevron icon in the corner of the menu. The trick
here is to link the click that occurs on the client, to the Toggle() event in the FoxPro code.
We us a client-side script to do so. That script is created using the CreateEventScript()
method, which is native to all Voodoo controls. Note that we have to link the created
script to the OnClick() event of the image we render.

There is one other issue to consider, that being state. Imagine the menu displays on the
same page as a data entry form. The user clicks the collapse button, which causes the
Expanded property to be set to False (.F.) and the control re-renders itself collapsed.
Then, the user clicks “Save” in the form, which causes the entire construct to be re-
created from scratch. This includes the Expanded property, which defaults to True (.T.).
This time there is no code running that would set that property to False (.F.), which
would now be more appropriate. Therefore, the menu expands as soon as the user clicks
“Save” (or causes any other even for that matter). Certainly, this is not the desired
behavior. We need Voodoo to remember the Expanded property and set its value
appropriately, rather than to its default.
Luckily, Voodoo can handle this for us. All we need to do is tell it about the Expanded
property:

   FUNCTION INIT
      THIS.PreserveProperty("Expanded")
      RETURN DODEFAULT()
   ENDFUNC

Note that it is very important to call DoDefault() here. Otherwise, you would lose a
whole lot of standard Voodoo functionality and your control would not work (which is
one of the reasons we introduced an OnLoad() event).

The only item left to explain is the AddMenuItem() method:

   FUNCTION AddMenuItem(lcItem,lcLink)
      THIS.cItems = THIS.cItems + ;
         [<li><a href="]+lcLink+[">]+;
         lcItem+[</a>]
   ENDFUNC

This method simply concatenates an HTML string and stores it in the cItems property.
Every time we add a menu item, the string stored in cItems grows. Whenever the control
renders, this string is included in the result set.

Of course, we are only scratching the surface of control creation here. I am sure you have
many questions. However, an in-depth explanation would be beyond the scope of this
whitepaper. For more information about this topic, please consult the “Voodoo Control
Developer SDK” when it becomes available.

Table Structure
The sample application uses only one table, named BUGS.ZIP. Here is the table structure
for this file:

Field Name            Type           Width
ID                    Character      10
TITLE                 Character      50
DATEENTERED           Date           8
REPO                  Memo           4
RESULTS               Memo           4
EXPECTED              Memo           4
CATEGORY              Character      20
OS                    Character      20
BROWSER               Character      20
SEV                   Integer        4

								
To top