Windows DevCenter    
 Published on Windows DevCenter (http://www.windowsdevcenter.com/)
 See this if you're having trouble printing code examples


Build a Web-Based Bug Tracking App, Part 2

by Jesse Liberty
05/23/2006

In a previous column, I began work on a very simple bug tracker that I could use to keep track of bugs (and their squishing) in my own small projects. In this column, I will add a number of additional features that make the product significantly more useful and that illustrate what I hope will be interesting techniques in building web-based applications.

  1. Grid will display the date the bug was originally reported, as well as the date it was last updated.
  2. Ability to filter for open bugs only.
  3. Ability to filter for "my bugs" only (bugs assigned to me).
  4. Implement cancel in the bug reporting page.
  5. Mark "show stopper" bugs in red.

In addition, the new code will enforce two new business rules:

  1. Once a bug is given a short description, that field can not be changed.
  2. Only QA can close a bug (the programmer can mark it "fixed," but only QA can mark it "closed".

A number of features will be hard-coded in the interest of making the article more readable.

Filtering Bugs and Displaying the Original Reported Date

To begin, you'll want to make a copy of the previous application (which you can download from my website) and, if you have not yet done so, use that same downloaded code to create (or restore) the database. Note that since the publication of the earlier article I have renamed the TimeRecorded field in BugHistories and have set it as a DateTime field that will be filled by parameters in the appropriate stored procedures.

Recording the Original Reported Date

To get started, modify the Bugs table, and add a single column, DateFirstRecorded, of the type DateTime. This will require a modification to the stored procedure spNewBug to ensure that the DateFirstRecorded field is populated when a new bug is created.

ALTER PROCEDURE [dbo].[spNewBug]
@ModifiedBy varchar(100),
@ShortDescription varchar(50),
@LongDescription ntext,
@Severity int,
@Notes ntext,
@Status int,
@Owner varchar(100),
@TimeRecorded datetime
AS
BEGIN
Begin Transaction 
   declare @bugID as int
   --declare @Placeholder as varchar(50)
   Insert into Bugs (PlaceHolder, DateFirstRecorded) 
   values (@ShortDescription, @TimeRecorded)
   select @bugID = @@identity
   if @@error <> 0 goto errorHandler
   insert into BugHistories ( BugID, BugHistoryID, TimeRecorded, ModifiedBy, 
      ShortDescription,LongDescription, Severity, Notes, 
      Status, Owner)
      values (
      @bugID, 1, @TimeRecorded, @ModifiedBy, @ShortDescription,
      @LongDescription, @Severity, @Notes, @Status, @Owner)
   if @@error <> 0 goto errorHandler
   commit transaction
   goto done
errorHandler:
   rollback transaction
done:
END

That done, the remaining changes all happen in the application itself.

As a start, add two check boxes to the TBTReview.aspx page: one to signify that you want to see only your own bugs, and the other to indicate that only open bugs should be displayed, as shown in Figure 1.

checkboxes
Figure 1. Checkboxes

Note that while I was at it, I changed the label control to display yellow, bold print on a red background; we'll use this later to indicate whether or not a record was updated or added.

Programming ASP.NET

Related Reading

Programming ASP.NET
Building Web Applications and Services with ASP.NET 2.0
By Jesse Liberty, Dan Hurwitz

The logic of filtering for my bugs and/or open bugs will be accomplished by modifying the select statement in the TBTDataSource control.

select bh.BugID, bh.BugHistoryID, ShortDescription, TimeRecorded, ModifiedBy, sev.Text as Severity,
stat.Text as Status, Owner, DateFirstRecorded from BugHistories bh
join Severities sev on sev.SeverityID = bh.Severity
join Statuses stat on stat.StatusID = bh.Status
join Bugs on bugs.bugID = bh.BugID
join (select bugId, max(bugHistoryId) as bugHistoryId from bughistories
  group by bugId) b
  on bh.bugId = b.bugId and bh.bugHistoryId = b.bugHistoryId
where Owner like  @Owner and Stat.Text <> @StatText
order by bh.bugid

The select statement now joins the bug table to select the DateFirstRecorded field. The new where statement uses the term like to match any bugs whose owner field matches the logged-in user's name. The second part of the where clause will exclude any matches that match the string (if any) that is passed in. For now, we'll hard code the string "Closed" if the appropriate check box is selected.

Unfortunately, this change to the DataSource control's schema requires rebuilding the columns for your grid, but that is quite all right, as we want to accommodate the DateFirstReported. Thus we re-name and re-order the fields in the grid as shown in Figure 2.

Thumbnail, click for full-size image.
Figure 2. Review grid (Click for full-size image)

Short Description is Read-Only

In practice, the short description (shown in the Bug column in Figure 2) acts as the "name" of the bug, and thus should not be modified when the bug is updated. The easiest way to accomplish this is to add a single line to the code that already exists to fill the fields with their existing values if we are updating an existing bug.

if (dv != null)  // yes it is a revision
{
    DataTable dt = dv.Table;

    // get the row for the latest BugHistory
    DataRow dr = dt.Rows[0];  
    //...
    this.txtShortDescription.Text = dr["ShortDescription"].ToString();
    this.txtShortDescription.ReadOnly = true;

Only QA Can Close A Bug

We have quickly accomplished requirements 1-5. Next, to ensure that only QA can close a bug, we'd like not to even offer the Close choice to anyone who is not in the QA role. The simplest way to accomplish this is to wait for the drop-down menu for the bug Status to be bound to its table, and then to delete the Closed element if the user is not in the QA group. We begin by adding an event handler for the DataBound event of the drop-down control, as shown in Figure 3.

dataBoundEvent
Figure 3. Adding the DataBound event

The event handler itself must check to see if the user is in the QA role, and if not, find the item in the list that contains the word closed and take it out of the list. The simplest approach is just to wait for the list to be filled (which will be true when the DataBound event is fired) and then iterate through the list until the item is located.

protected void ddlStatus_DataBound(object sender, EventArgs e)
{
    if (User.IsInRole("QA") == false)
    {
        // ddlStatus.Items.Remove("Closed");
        foreach (ListItem li in ddlStatus.Items)
        {
            if (li.Text.ToUpper() == "CLOSED")
            {
                ddlStatus.Items.Remove(li);
                break;
            }
        }
    }
}

Cancelling an Edit or New Bug

In the previous version, I never implemented the Cancel button in TBTReportBug.aspx. In this version, we'll do two things: we'll stash away a message to be displayed in the Review page, and we'll redirect to that page.

protected void btnCancel_Click(object sender, EventArgs e)
{
    Session["Message"] = "No action taken";
    Response.Redirect("TBTReview.aspx");
}

When you load the Review page, you'll check to see if this session variable exists; if so you'll display it in the label and then delete it.

lblMsg.Text = Session["Message"] != null ? 
    Session["Message"].ToString() : String.Empty;
Session.Remove("Message");

Note: this uses C#'s ternary operator (?:) and you can read this statement as "if the value in Session Message is not null, assign that value as a string to the label's text property; otherwise, set that property to the empty string."

Logging In and Out

To facilitate testing let's add a LoginStatus control to the master page, using only the LogOut link, as the control need not even be visible for users who are not logged in (done properly, you can't get to any of the other pages if you are not logged in). Drag a LoginStatus control next to the title, and set its LogoutPageURL to the Welcome page, its LogoutAction property to Redirect and its LogoutText property to "Log out." While we are at it, we can set the LoginText property to blank.

By assigning one user to the QA role, and other users to other roles (e.g., developer) we can edit bugs and quickly prove that only QA members can close a bug.

Marking Show-Stopper Bugs In Red

To fulfill the fifth and final requirement (marking show-stopper bugs in red), we need to create a method to handle the event raised when each data item is added to the grid. Click the Grid, click the lightning bolt, and then double-click in the RowDataBound event.

The event handler provided has a derived EventArgs object of the type GridViewRowEventArgs, which provides access both to the row in the grid and to the underlying data (e.Row.DataItem). In our case, however, we can simply access the row in the grid, find the sixth cell (which contains the status) and see if it meets our criteria. If so, we draw in bold and red:

protected void BugReviewGrid_RowDataBound(object sender, 
    GridViewRowEventArgs e)
{
    
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        TableCell cell = e.Row.Cells[5];
        if (cell.Text.ToUpper() == "SHOW STOPPER")
        {
            cell.Font.Bold = true;
            cell.ForeColor = System.Drawing.Color.Red;
        }
    }
}

The result is shown in Figure 4.

ShowStopper
Figure 4. Show stopper in red

This is a very effective way to find your most pressing bugs, and can be generalized to use color for many purposes in your application.

Hard Coding Versus Data-Driven

Einstein One of the premises of the Tiny Bug Tracker is that the code and the user interface are both kept as simple as possible. But the words of Einstein are worth keeping in mind: "Everything should be made as simple as possible, but not one bit simpler." [1]

We may have made it a bit simpler than it should be; however, by hard-coding the prohibited status as Closed, the privileged role as QA, and the highlighted status as Show Stopper. It would not be hard to create either a data table that held this information, or, perhaps even better, in the appSettings section of Web.config.

Let's create three settings:

  1. The role that has permission to close a bug.
  2. The string that indicates the status for closing a bug.
  3. The status string that should be shown in red.

To do so, open Web.config and add an appSettings tag if you don't have one (I had a self-closing one that I opened) and this code:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
  <appSettings>
    <add key="RoleThatMayCloseBug" value="QA" />
    <add key="StatusForClosedBug" value="Closed" />
    <add key="StatusToShowInRed" value="Show Stopper" />
  </appSettings>

This creates the keys and the values to be retrieved in the code. Modify the sections of code that use these values as follows: first, modify the if statement within the DataBound event handler in TBTReportBug.aspx.cs as follows:

 if (li.Text.ToUpper() == 
     ConfigurationManager.AppSettings
         ["StatusForClosedBug"].ToString().ToUpper())

That will fetch the word "Closed" from the Web.config file, and allow you to change the web config file without having to track down where this is used in the code. Similarly, we'll change the test for whether the user is in the role itself.

string theRole = ConfigurationManager.
    AppSettings["RoleThatMayCloseBug"].ToString();
if ( User.IsInRole(theRole) == false )

In the trade-off between redundancy and clarity, let's use a few more bits of your bandwidth to show the complete event handler, this time with the fetching from the ConfigurationManager done just once, at the top of the method.

protected void ddlStatus_DataBound(object sender, EventArgs e)
{
    string theRole = ConfigurationManager.
        AppSettings["RoleThatMayCloseBug"].ToString();
    
    string theStatus = ConfigurationManager.AppSettings
                    ["StatusForClosedBug"].ToString();

    if ( User.IsInRole(theRole) == false ) 
    {
        // ddlStatus.Items.Remove("Closed");
        foreach (ListItem li in ddlStatus.Items)
        {
            if ( li.Text.ToUpper() == theStatus.ToUpper() )
            {
                ddlStatus.Items.Remove(li);
                break;
            }
        }
    }
}

Finally, our third value is the status string to turn red, which is used in the BugReviewGrid_RowDataBound event handler in TBTReview.aspx.cs.

protected void BugReviewGrid_RowDataBound(object sender, 
    GridViewRowEventArgs e)
{
    string textToMatch =
        ConfigurationManager.AppSettings["StatusToShowInRed"].ToString();
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        TableCell cell = e.Row.Cells[5];
        if (cell.Text.ToUpper() == textToMatch.ToUpper())
        {
            cell.Font.Bold = true;
            cell.ForeColor = System.Drawing.Color.Red;
        }
    }
}

While this code may be a bit more cumbersome, I believe it is easier to maintain as it is crystal clear where these values are coming from (Web.config's AppSettings) and all of these "hard-coded" values are in a single place for easy maintenance.

[Footnote 1] This quote is rendered in various ways on the internet (and in hacker lore), all amounting to the same sentiment. WikiQuotes, however, gives the original as the somewhat more cumbersome "The supreme goal of all theory is to make the irreducible basic elements as simple and as few as possible without having to surrender the adequate representation of a single datum of experience."

Jesse Liberty is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.


Return to the Windows DevCenter.

Copyright © 2009 O'Reilly Media, Inc.