
Fig.1 Swat's Main Editing Page
Swat Part 4
This is the fourth article in a series describing the development of an application I devised as a learning project. The purpose of the project was to gain experience developing in the .NET environment. The goal I had given myself was to define a WEB-based application and then develop the application using ASP.NET. The articles describe my implementation solution for the application.
The application being developed is a full-featured bug tracking application. In this article we will be implementing the bug editing page. At the end of the next article we will have a fully functional application and will have completed the first phase's requirements. As in the previous articles I must state that even though the solutions that I present performs the required functionality, in some cases the solution presented may not be the most optimal one. As I stated in the beginning, my expectation is to learn from reader comments where a better solution exists.
SWAT's Place in the BIG Picture
Even though you are supposed to design in quality instead of testing for quality, testing is an essential part of product development. And an integral part of testing is reporting and tracking. That's where SWAT comes in, the controlling tool for the Test phase.
Testing should also be more than just functional verification. Part of the testing function should include �requirements verification� (you did have a requirements document, didn�t you?). One of the purposes of testing then, should be to compare what we said we would do to what we actually did. Check to see if we missed anything not just what�s not working.
Testing is just one of the phases of the development process. How testing is performed will have an impact on the overall success of the project. Likewise, following a development process does not by itself guarantee the success of a product but failure to follow one will certainly increase the probability of failure or as a minimum cost and schedule overruns. The development process does not have to be complex. It just needs to be a series of conscience steps like the following:
1.Concept->2.Validation/Design->3.Development->4.Test->5.Release (repeat 3,4,5 as needed or profitable)
What�s required for each phase will depend on the project and could be a very simple activity depending on the information available. Unfortunately, this is usually what happens on most development projects: 1 and 2 are skipped most of the time, 3 and 4 are usually combined (bad!), and 5 takes place at the point when the customer threatens with a lawsuit after the third missed ship date. This results in a lose-lose-lose situation.
Some pre-requisites
There's two related items that I'd like to cover before we get into the nitty-gritty of the bug editing page. The first is 'user-throttable' paging and the second is a side effect of providing 'user-throttable' paging.
In the admin page we hard coded the page size for the DataList to four entries. That was OK there because the number of items in any of the categories and the amount of time spent in the admin function should be small. For bugs it's a different story. There may be any number of bugs, and depending on the circumstance sometimes it may be beneficial to page five bugs at a time at other times you need to see twenty bugs at once. The paging scheme that we're going to implement for bugs is the same as for the admin page except that here the user will set the page size. The user will determine how fast/slow the paging will be, hence, 'user-throttable' paging.
A problem that comes up with allowing the user to control the size of the DataList is that any controls placed below the list are in the way. As the list grows it will overlay any items placed below it. Not good. One solution I found to resolve the problem was to implement the page as a table so that each section can grow automatically.
I just wanted to answer the question, "Why is the bug editing page laid out as a table?", ahead of time. Actually it's a table of tables. But it does what it's supposed to do.
Swat Bug Editing Page
The bug editing page is where the user enters, edits and changes the state of bugs. The UI for the page has three sections. The first section contains controls that allow the user to 'filter' what will be displayed. The second section consists of a list that will display the items that the user wants to see. And the third section contains a collection of controls that display the data associated with the currently selected item in the list. The filtering options include; the project the bugs were found in or to be added to, bugs assigned to the current user or all bugs, and bugs that are in a specific state. The other action that the user can perform is to change the state of a specific bug.
Refer to Fig.1 and let's start designing the page. Add a new WEB page to the project and name it SwatBugs. From the Visual Studio menu select 'Table->Insert...'. Make the table four rows by 2 columns. Each cel in the table is a sub-table and I�ve identified the configuration for each one below.
Table1(2rows/4cols) |
(empty) |
Table2(1row/1col) |
(empty) |
Table3(2rows/1col) |
Table4(6rows/2cols) |
Table5(2rows/4cols) |
(empty) |
Place the cursor in each of the table cells and then 'Table->Insert...' a table as defined above. For each sub-table drag-and-drop the items specified below.
Table1
Control |
ID |
Text |
Label |
lblSelProj |
Select Project |
Label |
lblSelFilt |
Select Filter Options |
Label |
lblPgSize |
#/PG |
DropDownList |
ddlProjects |
|
DropDownList |
ddlListFilter |
|
DropDownList |
ddlBugStates |
|
DropDownList |
ddlPageSize |
|
Button |
btnGetBugs |
Get Bugs |
Note, for the 'Select Filter Options' cell you can set the 'colSpan' property to '2' so that it will span two columns.
The project DropDownList will be loaded with the list of projects that have been defined in the database. The other DropDownLists have values hardcoded in them. If you select 'Items' from the properties for the DropDownList you'll be presented with a dialog allowing you to enter the items for the list as well as a value for each entry. Here's the value assignments for the DropDownLists in Table1.
ddlListFilter
ddlListFilter |
|
ListItem |
Value |
AllMyItems |
1 |
AllItems |
2 |
ddlBugStates
ddlBugStates |
|
ListItem |
Value |
Open Bugs |
1 |
Fixed Bugs |
2 |
Closed Bugs |
3 |
ddlPageSize
ddlPageSize |
|
ListIem |
Value |
5 |
5 |
10 |
10 |
15 |
15 |
20 |
20 |
Table2 just contains one item and that's a DataList. Drag one onto the table and accept the default ID. Start the PropertyBuilder for the DataList and set the Layout to �Flow� as we did on the admin page. Edit the Header/Footer Template and set the label to 'Bug Title'. In the footer drag and drop five Buttons. Set their properties as shown below:
ID |
Text |
CommandName |
btnPrev |
<< |
Prev |
btnCloseBug |
Close Bug |
CloseBug |
btnFixBug |
Fix Bug |
FixBug |
btnAddNew |
Add New |
AddNew |
btnNext |
>> |
Next |
The buttons in the footer are providing the same functionality as on the admin page. The command name gives you an idea of their purpose. Continue by editing the DataList item templates as follows :
Ctl Type |
ID |
CommandName |
Text |
Label |
lblBugID |
|
'<%# DataBinder.Eval(Container.DataItem, "id") %>' |
LinkButton |
lnkBugItem |
Select |
'<%# DataBinder.Eval(Container.DataItem, "itemname") %>' |
SelectedItem
Ctl Type |
ID |
CommandName |
Text |
LinkButton |
lnkEditBug |
Edit |
Edit |
Label |
lblSelID |
|
'<%# DataBinder.Eval(Container.DataItem, "itemname") %>' |
EditItem
Ctl Type |
ID |
CommandName |
Text |
LinkButton |
lnkEditUpdate |
Update |
Update |
LinkButton |
lnkEditCancel |
Cancel |
Cancel |
TextBox |
txtBugTitle |
|
'<%# DataBinder.Eval(Container.DataItem, "itemname") %>' |
Table3 has two items, a Label and a TextBox. Set the Label's id='lblDescription' and the 'Text' property to: 'Description: Enter all information describing the bug and how to reproduce the bug.'. The TextBox's id='txtDescription' and set the 'TextMode' property to 'MultiLine'.
Drag and Drop the following controls for Table4
CtlType |
ID |
Text |
Label |
lblEnteredBy |
Entered By |
TextBox |
txtEnteredBy |
|
Label |
lblEnteredDate |
Entered Date |
TextBox |
txtEnteredDate |
|
Label |
lblFixedBy |
Fixed By |
TextBox |
txtFixedBy |
|
Label |
lblFixedDate |
Fixed Date |
TextBox |
txtFixedDate |
|
Label |
lblClosedBy |
Closed By |
TextBox |
txtClosedBy |
|
Label |
lblClosedDate |
Closed Date |
TextBox |
txtClosedDate |
|
The TextBox controls above are set up as read-only but they could just as easily have been implemented using a Label control.
Here's the controls for Table5
CtlType |
ID |
Text |
Label |
lblSelMod |
Select Module |
Label |
lblAssignedTo |
Assigned To |
Label |
lblSeverity |
Severity |
Label |
lblPriority |
Priority |
Label |
lblRevision |
Revision |
DropDownList |
ddlModules |
|
DropDownList |
ddlSeverity |
|
DropDownList |
ddlPriority |
|
TextBox |
txtRevision |
|
We will load the module DropDownList with the modules that have been defined for the currently selected project. Here's the value assignments for the other DropDownLists in Table5.
ddlListFilter
ddlSeverity |
|
ListItem |
Value |
ShowStopper |
1 |
Level1 |
2 |
Level2 |
3 |
Level3 |
4 |
Enhancement |
5 |
ddlPriority |
|
ListItem |
Value |
1 |
1 |
2 |
2 |
3 |
3 |
4 |
4 |
5 |
5 |
Whew! I'm glad that's over with, now we can start making things do stuff. The first thing we need to do is provide the user with some means to filter the bugs that s/he wants to see. As a minimum the user is going to be interested in bugs for a specific project so we need to provide a list of available projects to select from. We're also going to give the user the option of seeing all of the bugs for the specified project or only the ones that have been assigned to him/her. The third option we'll provide the user is to select the state of the bugs to be viewed: all open bugs, all fixed bugs, or all closed bugs. Most of the time these options will be the same between sessions so we're going to persist them using cookies. All of this needs to be done when the page gets initialized so here's the code for the PageLoad event:
private void Page_Load(object sender, System.EventArgs e)
{
pagesize = System.Convert.ToInt16(ddlPageSize.SelectedItem.Value);
if (Request.Cookies["UserID"] != null)
{
Response.Cookies.Add(Request.Cookies["UserID"]);
Response.Cookies["UserID"].Expires = DateTime.MaxValue;
}
lblError.Text = "";
if (!Page.IsPostBack)
{
if (Request.Cookies["ListFilter"] != null)
{
Response.Cookies.Add(Request.Cookies["ListFilter"]);
Response.Cookies["ListFilter"].Expires = DateTime.MaxValue;
Response.Cookies["ListFilter"].Value =
Request.Cookies["ListFilter"].Value;
}
if (Request.Cookies["BugState"] != null)
{
Response.Cookies.Add(Request.Cookies["BugState"]);
Response.Cookies["BugState"].Expires = DateTime.MaxValue;
Response.Cookies["BugState"].Value =
Request.Cookies["BugState"].Value;
}
if (Request.Cookies["Project"] != null)
{
Response.Cookies.Add(Request.Cookies["Project"]);
Response.Cookies["Project"].Expires = DateTime.MaxValue;
Response.Cookies["Project"].Value =
Request.Cookies["Project"].Value;
}
try
{
SqlConnection cnn;
SqlCommand cmd;
SqlDataReader dr;
string ConnectionString =
"user id=ASPNET;password=;initial catalog=swatbugs;"
"data source=localhost;Integrated Security=false;"
"connect timeout=30;";
cnn = new SqlConnection(ConnectionString);
cmd = cnn.CreateCommand();
cnn.Open();
cmd.CommandText = "SELECT id, itemname FROM users";
dr = cmd.ExecuteReader();
ddlOwner.DataSource = dr;
ddlOwner.DataTextField = "itemname";
ddlOwner.DataValueField = "id";
ddlOwner.DataBind();
dr.Close();
if (Response.Cookies["BugState"].Value != null)
ddlBugStates.SelectedIndex =
ddlBugStates.Items.IndexOf(
ddlBugStates.Items.FindByValue(
Response.Cookies["BugState"].Value));
else
ddlBugStates.SelectedIndex = 0;
if (Response.Cookies["ListFilter"].Value != null)
ddlListFilter.SelectedIndex = ddlListFilter.Items.IndexOf(
ddlListFilter.Items.FindByValue(
Response.Cookies["ListFilter"].Value));
else
ddlListFilter.SelectedIndex = 0;
dr.Close();
cmd.CommandText = "SELECT * FROM projects";
dr = cmd.ExecuteReader();
ddlProjects.DataSource = dr;
ddlProjects.DataTextField = "itemname";
ddlProjects.DataValueField = "id";
ddlProjects.DataBind();
dr.Close();
}
catch(Exception ex)
{
lblError.Text = "Database Error.";
}
if (Response.Cookies["Project"].Value != null)
ddlProjects.SelectedIndex = ddlProjects.Items.IndexOf(
ddlProjects.Items.FindByValue(
Response.Cookies["Project"].Value));
else
ddlProjects.SelectedIndex = 0;
cnn.Close();
ViewState["curpage"] = 1;
ViewState["pagecount"] = 0;
BindModuleCB();
BindBugList("0",ScrollMode.UpdateNextPage);
EnableEditing(false);
SetTotalPages();
}
pagecount = (int)ViewState["pagecount"];
}
So, the first time the page is loaded we check to see if we have any stored preferences for the user by checking if the cookies exists. Then we get a list of all users that have been defined in the database so we can load the 'owners' DropDownList. This list provides several purposes. First, when a bug is being created the bug can be assigned to a person by selecting from this list and when a bug is selected for viewing the owner is displayed on this list. The list is also used as a local cache of the users so we don't have to re-read the database to get the user's name to fill in the 'OpenedBy',etc. fields.
We load the module's DropDownList but we use a helper method for that because we also need to load the list if the users changes the selected project. The other functionality you should recognize as being the same as was done on the admin page.
You may also find interesting how the code above sets the selected index in the DropDownLists if a respective cookie was found.
Since we're using some external classes we need to add the following declarations to the top of the SwatBugs code source.
...
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Text
User-throttable paging
The user interface for this page is similar to how the admin page was designed. We have a DataList that displays the title of the bugs so that the user can select the item of interest. The paging mechanism is the same except that we're allowing the user to specify how many items to display on each page. We're keeping track of the user's choice through the 'pagesize' variable. And as we did in the admin page we are using a 'pagecount' and 'currentpage' ViewState variables. The SetTotalPages method performs the same function as it did on the admin page. Here's the code:
protected void SetTotalPages()
{
try
{
SqlConnection cnn;
string ConnectionString = "user id=ASPNET;"
"password=;initial catalog=swatbugs;"
"data source=localhost;Integrated Security=false;"
"connect timeout=30;";
cnn = new SqlConnection(ConnectionString);
cnn.Open();
StringBuilder sqlString =
new StringBuilder("SELECT Count(*) FROM bugs WHERE ");
sqlString.Append("Project=@projectid AND ");
sqlString.Append("Status=@statusid ");
if (ddlListFilter.SelectedItem.Text != "All Items")
sqlString.Append("AND AssignedTo=@ownerid");
SqlCommand cmd = cnn.CreateCommand();
cmd.CommandText = sqlString.ToString();
cmd.Parameters.Add("@projectid", SqlDbType.Int).Value =
ddlProjects.SelectedItem.Value;
cmd.Parameters.Add("@statusid", SqlDbType.Int).Value =
ddlBugStates.SelectedItem.Value;
if (ddlListFilter.SelectedItem.Text != "All Items")
cmd.Parameters.Add("@ownerid", SqlDbType.Int).Value =
Response.Cookies["UserID"].Value;
int nRecords = (int)cmd.ExecuteScalar();
if (nRecords%pagesize == 0)
{
pagecount = nRecords / pagesize;
}
else
{
pagecount = (nRecords / pagesize) + 1;
}
ViewState["pagecount"] = pagecount;
ViewState["curpage"] = 1;
cnn.Close();
}
catch(Exception e)
{
lblError.Text = "Database Error.";
}
}
Each project is composed of one or more modules. Modules can be anything the user defines them to be, an executable, a page, a control, etc. The user defined the modules that make up a project in the admin page. Now we need to show the list of modules available for the selected project so that the user can enter bugs for that module or indicate in which module a bug was found. The BindModuleCB() method does that. As I mentioned above, the paging mechanism used on this page is almost identical to how it was implemented on the admin page. We keep track of which direction we�re paging or if we need to �page in place� as a result of the user simply editing an item or deleting an entry, we keep track of how many items we are to display on a page, what our current page is, and how many pages are actually in the database. When we query the database we extract the just the number we need for a page and base it on the current top item being displayed (ID). Here's the code for BindModuleCB() and BindBugList() methods.
protected void BindModuleCB()
{
try
{
SqlConnection cnn;
SqlCommand cmd;
SqlDataReader dr;
string ConnectionString = "user id=ASPNET;password=;"
"initial catalog=swatbugs;data source=localhost;"
"Integrated Security=false;connect timeout=30;";
cnn = new SqlConnection(ConnectionString);
cmd = cnn.CreateCommand();
cnn.Open();
StringBuilder sqlString = new StringBuilder(
"Select * from Modules where Project=");
sqlString.Append(ddlProjects.SelectedItem.Value);
cmd.CommandText = sqlString.ToString();
dr = cmd.ExecuteReader();
ddlModules.DataSource = dr;
ddlModules.DataTextField = "itemname";
ddlModules.DataValueField = "id";
ddlModules.DataBind();
cnn.Close();
}
catch(Exception e)
{
lblError.Text = "Database Error.";
}
}
private void BindBugList(string sRecNum, ScrollMode direction)
{
try
{
SqlConnection cnn;
string strDirection = ">=";
StringBuilder sqlString = new StringBuilder("SELECT TOP ");
sqlString.Append(pagesize.ToString());
sqlString.Append(" id, itemname FROM ");
switch(direction)
{
case ScrollMode.UpdateInPlace:
strDirection = ">=";
break;
case ScrollMode.UpdateNextPage:
strDirection = ">";
break;
case ScrollMode.UpdatePrevPage:
strDirection = "<";
break;
}
string ConnectionString = "user id=ASPNET;password=;"
"initial catalog=swatbugs;data source=localhost;"
"Integrated Security=false;connect timeout=30;";
cnn = new SqlConnection(ConnectionString);
cnn.Open();
sqlString.Append("bugs WHERE ID");
sqlString.Append(strDirection);
sqlString.Append(sRecNum);
sqlString.Append(" AND Project=@projectid AND ");
sqlString.Append("Status=@statusid ");
if (ddlListFilter.SelectedItem.Text != "All Items")
sqlString.Append("AND AssignedTo=@ownerid");
if (direction == ScrollMode.UpdatePrevPage)
sqlString.Append(" ORDER BY id DESC");
SqlCommand cmd = cnn.CreateCommand();
cmd.CommandText = sqlString.ToString();
cmd.Parameters.Add("@projectid", SqlDbType.Int).Value =
ddlProjects.SelectedItem.Value;
cmd.Parameters.Add("@statusid", SqlDbType.Int).Value =
ddlBugStates.SelectedItem.Value;
if (ddlListFilter.SelectedItem.Text != "All Items")
cmd.Parameters.Add("@ownerid", SqlDbType.Int).Value =
Response.Cookies["UserID"].Value;
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataSet ds = new DataSet();
da.Fill(ds,"BUGS");
ds.Tables["BUGS"].DefaultView.Sort = "id ASC";
DataList1.DataSource = ds.Tables["BUGS"].DefaultView;
DataList1.DataKeyField = "ID";
DataList1.DataBind();
cnn.Close();
}
catch(Exception e)
{
lblError.Text = "Database error.";
}
}
Bugs have more data than just the title so we're going to programmatically control access to these additional fields. When the user is �browsing� the bugs, the controls that contain the data will be set to �read-only� mode. Only when the user is editing a specific bug or entering a new bug will these controls be enabled. Here�s the method that controls the visibility of these controls.
private void EnableEditing(bool bShow)
{
btnGetBugs.Enabled = !bShow;
txtDescription.Enabled = bShow;
txtRevision.Enabled = bShow;
ddlPriority.Enabled = bShow;
ddlSeverity.Enabled = bShow;
ddlOwner.Enabled = bShow;
ddlModules.Enabled = bShow;
}
Just as we did with the admin DataList we need to control the visibility of the controls embedded in the footer. The code is slightly different here (for no reason) in that I�m maintaining a reference to the controls in the class definition. The controls are �found� when the DataList creates it�s children (in the DataList ItemCreated() event) and we control their visibility in the Pre-Render() method. Here�s the code for those two methods (which you can add as we did on the admin page), but first add the following to the SwatBugs class definition:
public class SwatBugs : System.Web.UI.Page
{
public enum BugState
{
Bug_Open = 1,
Bug_Fixed,
Bug_Closed
}
private Control ctlCloseBug;
private Control ctlFixBug;
private Control ctlAddNew;
private Control ctlPrev;
private Control ctlNext;
int pagesize = 5;
int pagecount;
...
}
private void Page_PreRender(object sender, System.EventArgs e)
{
bool bEditing = false;
if (DataList1.EditItemIndex == -1)
{
ctlAddNew.Visible = true;
ctlPrev.Visible = true;
ctlNext.Visible = true;
}
else
{
bEditing = true;
ctlPrev.Visible = false;
ctlNext.Visible = false;
ctlAddNew.Visible = false;
}
if (DataList1.SelectedIndex >= 0 && bEditing == false)
{
if (System.Convert.ToInt16(
ddlBugStates.SelectedItem.Value) ==
(int)BugState.Bug_Open)
{
ctlCloseBug.Visible = false;
ctlFixBug.Visible = true;
}
else
{
if (System.Convert.ToInt16(
ddlBugStates.SelectedItem.Value) ==
(int)BugState.Bug_Fixed)
{
ctlCloseBug.Visible = true;
ctlFixBug.Visible = false;
}
else
{
ctlCloseBug.Visible = false;
ctlFixBug.Visible = false;
}
}
}
else
{
ctlCloseBug.Visible = false;
ctlFixBug.Visible = false;
}
int curpage = (int)ViewState["curpage"];
if (curpage > 1)
((Button)ctlPrev).Enabled = true;
else
((Button)ctlPrev).Enabled = false;
if (curpage < pagecount && curpage != pagecount)
((Button)ctlNext).Enabled = true;
else
((Button)ctlNext).Enabled = false;
}
private void DataList1_ItemCreated(object sender,
System.Web.UI.WebControls.DataListItemEventArgs e)
{
if (e.Item.ItemType == System.Web.UI.WebControls.ListItemType.Footer)
{
ctlCloseBug = ((Control)(e.Item)).FindControl("btnCloseBug");
ctlFixBug = ((Control)(e.Item)).FindControl("btnFixBug");
ctlAddNew = ((Control)(e.Item)).FindControl("btnAddNew");
ctlPrev = ((Control)(e.Item)).FindControl("btnPrev");
ctlNext = ((Control)(e.Item)).FindControl("btnNext");
}
}
OK, now we�re ready to start coding the user�s actions which are all going to come through the DataList�s command messages. Add an event handler for the DataList�s ItemCommand event (from the DataList's properties pane select events and then double-click the desired event). This is literally 'command central'.
private void DataList1_ItemCommand(object source,
System.Web.UI.WebControls.DataListCommandEventArgs e)
{
EnableEditing(false);
ViewState["selitem"] = -1;
if (e.CommandName == "Prev")
{
}
if (e.CommandName == "Next")
{
}
if (e.CommandName == "AddNew")
{
}
if (e.CommandName == "FixBug")
{
}
if (e.CommandName == "CloseBug")
{
}
if (e.CommandName == "Select")
{
ViewState["selitem"] = e.Item.ItemIndex;
DataList1.SelectedIndex = e.Item.ItemIndex;
DataList1.EditItemIndex = -1;
BindBugList(DataList1.DataKeys[0].ToString(),
ScrollMode.UpdateInPlace);
}
if (e.CommandName == "Edit")
{
DataList1.EditItemIndex = e.Item.ItemIndex;
BindBugList(DataList1.DataKeys[0].ToString(),
ScrollMode.UpdateInPlace);
}
if (e.CommandName == "Cancel")
{
DataList1.SelectedIndex = -1;
DataList1.EditItemIndex = -1;
BindBugList(DataList1.DataKeys[0].ToString(),
ScrollMode.UpdateInPlace);
}
if (e.CommandName == "Update")
{
}
}
As we did on the admin page we'll remove the comments as we enable the functionality.
Conclusion
Well, I need a break and this is as good a stopping point as any. You can compile and run the application to check for any editing mistakes. If you had created any users with 'Developer' role, then when you run the application it will automatically start on the bug editing screen. You won't be able to do much but you can verify the code that we have so far. The only button enabled on the DataList footer should be the 'AddNew' button. Which is the first thing the user is going to want to do. If you added any projects they should appear on the projects DropDownList. And all of the bug data controls should be disabled. That's actually quite a bit. You can also check to see that if you defined a user with 'Developer' only role that he can't access the admin page.
The next article will complete the design for the bug editing page and at that point we'll have a fully functional bug tracking application.