Click here to Skip to main content
Click here to Skip to main content

SWAT - A simple Web-based Anomalies Tracker - Part 8

, 23 Sep 2003 CPOL
Rate this:
Please Sign up or sign in to vote.
An account of my experience in learning to develop in the .NET environment

Fig.1 Swat's Analysis Page

Swat Part 8

This is the penultimate SWAT article. Only one more to complete the goals described in part 1. I'll share a secret with you, writing the articles was way harder than coding the original application. But I suppose that's also a learning experience. It's been fun and I hope that it's provided some benefit to the readers. Perhaps the application itself has found a few servers to run on. If anyone is using it and has made any enhancements please let me know so everyone can benefit or better yet write an article!

A quick note on installing the latest version. If you have been using the application and have data in the database make sure you export the data before installing the new version and running the scripts. Just to be on the safe side.

Here's the verbage for those who are reading this as the first SWAT article. This series of articles describe 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.

SWAT's Analysis Feature

Last time we pretty much beat to death the reasons for the analysis feature. So let's get right down to business. There's not much to the coding on this page since most of the work is relegated to the stored procedures and the charting control. About the only 'neat' feature that I can point to is the fact that the images are being created dynamically, they are not static images that are stored on the server.

Add a new WebPage to the project and name it SwatAnalyse. Refer to Fig.1 and add the controls and set their properties as shown below. This page will 'drive' the rendering. We'll add another page below that will generate the image (with help from the charting control).

Control

ID

Text

Label

lblSelChart

Select Chart

Label

lblSelProject

Select Project

Label

lblSelModule

Select Module

Label

lblSelSeverity

Select Severity

Label

lblMessage

Button

btnGetChart

Get Chart


Control

ID

Width

Height

ImgUrl

Image

imgSwatChart

488

328

SwatChart.aspx


Control

ID

DropDownList

ddlChart

MasterDetail.MasterDDL

ddlProjects

MasterDetail.DetailDDL

ddlModules

DropDownList

ddlSeverity

You'll notice that we've made use of the MasterDetailDDL that I mentioned in the last article. Make sure sure that you have added a reference to the control and that you added it to your Toolbox. This control will allow the user to make all selections for the chart without having to make a trip to the server. Place the 'lblMessage' control above the image control and size it as wide. You can't tell from the image in Fig.1 that there's room there for a label. But that's 'cause I had to edit it a little to fit the CP requirements. You will also need to set the 'Visible' property to 'false'. We'll use the lblMessage control to present a message to the user when needed. So let's begin, add the following enum to the SwatAnalyse class definition. This defines the four basic graph types that we'll implement.

public enum GraphMode
{
  GraphModuleStatus = 1,
  GraphProgress,
  GraphActivity,
  GraphProjStatus
}

As with all the other pages, we need to check during Page_Load() if the user actually has privileges to access this page. Then we do whatever housekeeping needs to be done for the page. The comments indicate what's happening.

private void Page_Load(object sender, System.EventArgs e)
{
  int nRole = (int)AccessPrivilege.Manager;
  if (Request.Cookies["Roles"] != null)
  {
    nRole = System.Convert.ToInt16(Request.Cookies["Roles"].Value);
    if ((int)AccessPrivilege.Manager != 
       (nRole & (int)AccessPrivilege.Manager))
      Response.Redirect("SwatBugs.aspx",true);
  }
  else
    Response.Redirect("SwatLogon.aspx",true);

  //We know we have this because the user is logged in
  if (Request.Cookies["UserID"] != null)
  {
    Response.Cookies.Add(Request.Cookies["UserID"]);
    Response.Cookies["UserID"].Expires = DateTime.MaxValue;
  }
  if (!Page.IsPostBack)
  {
    //Dynamically adding an item to a DropDownList...
    //Add an "All" selection to the severity combo
    ListItem ls = new ListItem("All","0");
    ddlSeverity.Items.Add(ls);
    ddlSeverity.SelectedIndex = System.Convert.ToInt32(
       ddlSeverity.Items.IndexOf(ddlSeverity.Items.FindByText("All")));

    //Populate the projects combo
    BindProjectCB();

    //Set a default chart
    ViewState["mode"] = (int)GraphMode.GraphModuleStatus;
    
    imgSwatChart.Visible = false;

    //First time, tell user to select something
    lblMessage.Text = "Select options to plot.";
    lblMessage.Visible = true;
  }
  else
  {
    imgSwatChart.Visible = true;
    //Populate the projects combo
    BindProjectCB();
    lblMessage.Visible = false;
  }
}

The MasterDetailDDL control requires that we create a table of the master/slave information to be displayed. In our case the database tables are configured the way the control expects them so we don't need to do anything else. If they weren't, we would need to do a table join in order to get the required configuration. The MasterDetailDDL article discusses this as well as providing an example.

protected void BindProjectCB()
{
  SqlConnection cnn;
  SqlCommand cmd;
  SqlDataReader dr;
  
  string ConnectionString = ConfigurationSettings.AppSettings["dbconn"];
  cnn = new SqlConnection(ConnectionString);
  cmd = cnn.CreateCommand();
              
  cnn.Open();
  cmd.CommandType = CommandType.StoredProcedure;
  cmd.CommandText = "SWATGetAllProjects";
  dr = cmd.ExecuteReader();
  ddlProjects.DataSource = dr;
  ddlProjects.DataTextField = "itemname";
  ddlProjects.DataValueField = "id";
  ddlProjects.DataBind();
  dr.Close();

  if (Response.Cookies["Project"].Value != null)
    ddlProjects.SelectedIndex = ddlProjects.Items.IndexOf(
      ddlProjects.Items.FindByValue(Response.Cookies["Project"].Value));
  else
    ddlProjects.SelectedIndex = 0;

  cmd.CommandText = "SWATGetAllModules";
  SqlDataAdapter da = new SqlDataAdapter(cmd);
  
  //Setup the MasterDetail DropDownList control
  DataSet ds = new DataSet("ProjMod");
  da.Fill(ds,"MODULES");
  Array Data;
  Data = Array.CreateInstance(typeof(object),1,3);
  Data.SetValue("ddlModules",0,0);
  Data.SetValue(ds,0,1);
  Data.SetValue("MODULES",0,2);
  ddlModules.TableName = "MODULES";
  ddlProjects.SlaveData = Data;
  cnn.Close();
}

Embedded Pages

So when the browser loads the SwatAnalyse page it sees that one of the controls is an image which needs to loaded. It reads the URL of the image and proceeds to load the item from the specified location. Normally there would be a path to where the image is located. In our case we're actually referencing an ASP page. When that page gets requested the code on that page will dynamically generate an image and return it to the browser. The browser doesn't know or care that it's not an image URL that it's pointing to. I guess all it cares is that whatever is returned better be in a format it knows how to handle.

That's all pretty straight forward so far, but there's a little bit of trickery required in order to achieve what we want. In order for the referenced page to generate the image we want it needs to know what it is that we want. That means that we have to pass some parameters to the page to tell it what we want generated. And that's what we need to do now. Add an event handler for the 'GetChart' button and revise as follows.

private void btnGetChart_Click(object sender, System.EventArgs e)
{
  StringBuilder strParameters = new StringBuilder("SwatChart.aspx?mode=");
  strParameters.Append(ddlChartType.SelectedItem.Value);
  strParameters.Append("&proj=");
  strParameters.Append(ddlProjects.SelectedItem.Value);
  strParameters.Append("&mod=");
  strParameters.Append(ddlModules.SelectedItem.Value);
  strParameters.Append("&sev=");
  strParameters.Append(ddlSeverity.SelectedItem.Value);

  imgSwatChart.ImageUrl = strParameters.ToString();
}

Basically what we are doing is packaging up the users selection to pass as named 'arguments' to the image URL.

Image is everything

Add another WebPage to the project and name it SwatChart. This is the page that will be called to do the actual rendering of the chart (with help from the charting control). In other words it will return an image file.

So let's start with the Page_Load(). Since this page is expecting some arguments, the first thing we do is check for the 'mode' argument to see which chart we are to generate. If we didn't get that named argument then we just return a message indicating so. Otherwise we just call the appropriate method to generate the chart the user selected.

private void Page_Load(object sender, System.EventArgs e)
{
  Response.Clear();

  //What's the request
  if (Request.QueryString["mode"] != null && 
    Request.QueryString["mode"].Length != 0)
  {
    string strMessage="";
    int nMode = Convert.ToInt16(Request.QueryString["mode"]);
    switch(nMode)
    {
      case (int)GraphMode.GraphModuleStatus:
        if (DoGraphModuleStatus(ref strMessage) == false)
          DisplayMessage(strMessage);
        break;
      case (int)GraphMode.GraphProgress:
        if (DoGraphProgress(ref strMessage) == false)
          DisplayMessage(strMessage);
        break;
      case (int)GraphMode.GraphActivity:
        if (DoGraphActivity(ref strMessage) == false)
          DisplayMessage(strMessage);
        break;
      case (int)GraphMode.GraphProjStatus:
        if (DoGraphProjectStatus(ref strMessage) == false)
          DisplayMessage(strMessage);
        break;
    }
  }
  else
    DisplayMessage("Sumpin' went wrong!");
}

All four charting methods sorta kinda do the same thing in just a slightly different way. Each just varies by the data that needs to be loaded from the database and which chart object type we're going to instantiate. So I'm just going to describe one of them and you can look at the others from the download for any specific interest.

The progress graph wants to plot the number of bugs that exist in each state for each day available in the database. That is, from the first day that a bug was entered (for the specified project) until the last bug change state that exists in the database (for the specified project). We basically want to plot the total number of bugs open/fixed/closed over a period of time. So the first thing we need to do is determine the time span we're dealing with. The start date is easy since we know that a bug has to be entered before it can be fixed or closed. The end date is a little harder to get since it can be a bug in any of the three states. For example if today was the last date, a bug could have been opened today, or an existing bug could have been fixed or closed. So we have to check all three states to get the last 'activity' date.

The charting control requires that we pass in information defining the series to be plotted. So that's what we do next. This will tell the control how many series are to be plotted, as well as the color and label to use for each series.

Then we create an array to hold the data to be plotted, instantiate the appropriate charting object and tell it to draw the chart. And that's it. Just for readability, we relegate the actual loading of the data to a helper function.

private bool DoGraphProgress(ref string strMessage)
{
  int nProjID;
  if (Request.QueryString["proj"] != null && 
     Request.QueryString["proj"].Length != 0)
    nProjID = Convert.ToInt32(Request.QueryString["proj"]);
  else
  {
    strMessage = "Select option to plot.";
    return false;
  }
  try
  {
    SqlDataReader dr;
    DateTime dt1 = DateTime.Now;
    DateTime dt2 = DateTime.Now;

    //Get the database data
    //First need to find the date range for the bugs
    SqlConnection cnn;
    string ConnectionString = ConfigurationSettings.AppSettings["dbconn"];
    cnn = new SqlConnection(ConnectionString);
    cnn.Open();

    SqlCommand cmd = cnn.CreateCommand();
    cmd.CommandType = CommandType.StoredProcedure;

    //entereddate will give us the start date
    cmd.CommandText = "SWATGetFirstEnteredDate";

    // Fill our parameters
    cmd.Parameters.Add("@projid", SqlDbType.Int).Value = nProjID;

    dr = cmd.ExecuteReader();
    if (dr.Read())
      dt1 = (DateTime)dr[0];
    else
    {
      strMessage = "Nothing in the DB.";
      return false;  //we can't do anything...
    }

    //but the end date could be an enteredate, fixeddate, or closeddate
    dr.Close();
    cmd.CommandText = "SWATGetLastDate";

    dr = cmd.ExecuteReader();
    if (dr.Read())
    {
      if (dr[0] != null && !dr[0].Equals(System.DBNull.Value))
        dt2 = (DateTime)dr[0];
      if (dr[1] != null && !dr[1].Equals(System.DBNull.Value))
      {
        if (dt2 < (DateTime)dr[1])
          dt2 = (DateTime)dr[1];
      }
      if (dr[2] != null && !dr[2].Equals(System.DBNull.Value))
      {
        if (dt2 < (DateTime)dr[2])
          dt2 = (DateTime)dr[2];
      }

      //Set the series name and color
      Array SeriesInfo;
      SeriesInfo = Array.CreateInstance(typeof(object),3,2);

      SeriesInfo.SetValue("Open",0,0);
      SeriesInfo.SetValue(Color.Red,0,1);
      SeriesInfo.SetValue("Fixed",1,0);
      SeriesInfo.SetValue(Color.Blue,1,1);
      SeriesInfo.SetValue("Closed",2,0);
      SeriesInfo.SetValue(Color.Green,2,1);

      TimeSpan numDays = dt2 - dt1;
      float fMax = 0F;
      Array Data;
      //We'll store the data as a 2-dim array, the first element of
      //each row will be a label, if any. Followed by each series data
      Data = Array.CreateInstance(typeof(object),numDays.Days+1,4);

      //Get the data from the database and store it in the array
      GetProgressData(dt1,dt2,ref Data,ref fMax);

      //Create the line chart object
      SwatChartLib.SwatLineChart ch = new SwatChartLib.SwatLineChart(
           this,400F,300F);
      //Set the XAxis label
      StringBuilder strDateRange = new StringBuilder();
      strDateRange.Append(dt1.ToShortDateString());
      strDateRange.Append("-");
      strDateRange.Append(dt2.ToShortDateString());
      ch.strXaxisLabel = strDateRange.ToString();
      //Set the YAxis label
      ch.strYaxisLabel = "# of Bugs";
      //Draw the graph
      ch.DrawChart(SeriesInfo,Data,fMax,1F,1F);
    }

    cnn.Close();
  }
  catch(Exception e)
  {
    strMessage = e.ToString();
    return false;
  }
  return true;
}

And here's the helper function that actually loads the data from the database. You can follow the comments but basically all we're doing is iterating through the date span and getting the bug count for each bug state.

private void GetProgressData(DateTime Start, DateTime End,ref Array Data,
    ref float fMax)
{
  SqlConnection cnn;
  string ConnectionString = ConfigurationSettings.AppSettings["dbconn"];
  cnn = new SqlConnection(ConnectionString);
  cnn.Open();

  SqlCommand cmd = cnn.CreateCommand();
  cmd.CommandType = CommandType.StoredProcedure;

  DateTime DateCounter = Start;

  float fOpen = 0F;
  float fFixed = 0F;
  float fClosed = 0F;
  int nCount = 0;

  cmd.Parameters.Add("@currdate", SqlDbType.DateTime);

  fMax = 1F;
  //Get the number of bugs for each day in the range. We want the 
  //total for each state.
  while(DateCounter <= End)
  {
    cmd.Parameters["@currdate"].Value = DateCounter;
    cmd.CommandText = "SWATGetOpenBugCount";
  
    //No label
    Data.SetValue("",nCount,0);

    fOpen = System.Convert.ToSingle(cmd.ExecuteScalar());
    Data.SetValue(fOpen,nCount,1);
    cmd.CommandText = "SWATGetFixedBugCount";
    fFixed = System.Convert.ToSingle(cmd.ExecuteScalar());
    Data.SetValue(fFixed,nCount,2);
    cmd.CommandText = "SWATGetClosedBugCount";
    fClosed = System.Convert.ToSingle(cmd.ExecuteScalar());
    Data.SetValue(fClosed,nCount,3);
    DateCounter = DateCounter.AddDays(1);
    nCount++;

    //We know the open bugs has to be the largest one
    if (fOpen > fMax)
      fMax = fOpen;
  }
}

When all you have is a hammer

Since all we are creating is an image, we don't have any facilities to return textual information to the user. For example, in the case of an error. The following method creates a message as an image so that we can return a message to the user.

protected void DisplayMessage(string sMess)
{
  
  Bitmap bmp = new Bitmap(m_nImageWidth, m_nImageHeight, 
     PixelFormat.Format24bppRgb);
  Graphics g = Graphics.FromImage(bmp);

  g.SmoothingMode = SmoothingMode.None;
  g.TextRenderingHint = TextRenderingHint.SystemDefault;
  g.Clear(Color.White);

  System.Drawing.RectangleF rcMess = new System.Drawing.RectangleF();
  Font fontMess = new Font("Arial", 16);

  SizeF szMess = g.MeasureString(sMess,fontMess);
  rcMess.Size = szMess;
  rcMess.X = (m_nImageWidth - szMess.Width)/2;
  rcMess.Y = (m_nImageHeight - szMess.Height)/2;

  g.DrawString(sMess
        ,fontMess
        ,new System.Drawing.SolidBrush(Color.Black)
        ,rcMess);

  MemoryStream stream = new MemoryStream();
  bmp.Save(stream, ImageFormat.Png);

  this.Response.Clear();
  this.Response.ContentType = "image/png";
  this.Response.BinaryWrite(stream.ToArray());

  g.Dispose();

  this.Response.End();
}

Logout Feature

There are a few buttons on the SWAT toolbar that have not been implemented. One of them is the 'Logout' button. Let's quickly implement that to end this article. Add a new Web page and name it SwatLogoff. This page doesn't need to do much, only to remove our authentication cookie from the client's machine. I also added a script that will prompt the user and close the 'application' window. Here's the code for the Page_Load() event.

private void Page_Load(object sender, System.EventArgs e)
{
  Response.Cookies.Clear();
  FormsAuthentication.SignOut();
  StringBuilder strScript = new StringBuilder();
  strScript.Append(
     "<SCRIPT FOR=window EVENT=onload LANGUAGE="\""JScript\">\n");
  strScript.Append("top.close();\n");
  strScript.Append("</SCRIPT>");

  Page.RegisterClientScriptBlock("ClientScript",strScript.ToString());
}

And you'll need to make the following change to the Toolbar page HTML code to hook in the button.

        ...
  <td><a onmouseover = 
     "ChangeImages('TbLogoutImage', 'Toolbar_Logout_hover.gif'); 
     return true;" 
    href="SwatLogoff.aspx" target="_top"...
        ...

The fat lady is warming up.

Well that's it. The analysis feature is completed and you will be able to generate all those 'nifty' (how's that for a throwback) graphs. Of course you'll need to generate some bugs in order to make them interesting. But I think we won't run out of bugs in our code any time soon. Next time we'll add the reporting feature and I'll be done. At least with the goals I had given myself in the first article.

License

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

Share

About the Author

Al Alberto
Software Developer (Senior)
United States United States
No Biography provided

Comments and Discussions

 
GeneralSwatAdmin images PinmemberHlodver29-Oct-03 2:52 
General[Message Deleted] PinsussTref Gare22-Oct-03 17:40 
GeneralRe: Part 8 ? SwatReports?? PinmemberAl Alberto23-Oct-03 2:06 
Generalmasterdetail missing Pinmembermdissel24-Sep-03 10:50 
GeneralRe: masterdetail missing PinmemberAl Alberto24-Sep-03 11:56 
GeneralRe: masterdetail missing Pinmembermdissel24-Sep-03 22:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 24 Sep 2003
Article Copyright 2003 by Al Alberto
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid