Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP.NET 2.0 Instant Results

.pdf
Скачиваний:
67
Добавлен:
17.08.2013
Размер:
11.03 Mб
Скачать

The Bug Base

With all the required variables set up, it’s time to file an actual bug. If you choose File New Bug from the main Bugs menu, the AddEditBug.aspx page located in the Bugs folder appears. This page is shown in Figure 12-3 at the beginning of this chapter.

Theoretically, the form on that page would have been an ideal candidate for the new <asp:FormView> control that allows you to quickly set up an Insert and Edit page. All you need to do is bind the FormView control to a few methods in your business layer, and Visual Web Developer will create the necessary insert and edit templates for you. However, the way the Bug class is designed proves to be problematic for the FormView. By design, the FormView can only work with direct properties such as the Bug’s Title or Description. However, some of the Bug’s properties are actually NameValue objects of which the FormView has no knowledge. Because of this lack of knowledge, the FormView isn’t able to correctly bind to the data stored in the Bug object. Future versions of the .NET Framework may bring direct support for more complex properties like the NameValue object, but until that time you need to work around these limitations. Although there are ways to make the FormView work with the NameValue objects, the amount of code required to make that work isn’t worth the benefit of the FormView in the first place. That’s why the Insert and Update forms were built as a regular form with text boxes and drop-down controls nested in an HTML table. If you do decide to implement a FormView to bind to objects with complex custom properties, the trick is to use Eval in your binding syntax in the .aspx portion of the page instead of Bind. Then in the code-behind you can write code for the FormView control’s ItemInserting and ItemUpdating events and create and assign new instances of your custom objects to the e.Values or e.NewValues properties of the arguments of the Inserting and Updating methods.

The AddEditBug.aspx page can be viewed in two different ways — one where each of the controls like the drop-downs are editable, and one where most of the controls have been replaced with static labels. The first view is used when any user is filing a new bug or when a developer or manager is editing a bug. The second view is used when a tester is editing a bug. Once a bug has been filed, a tester can no longer change the properties of a bug, so all controls are replaced with static text, showing the underlying values.

Determining which controls to show and which to hide takes place in the LoadData method, which is discussed after the exploration of Page_Load.

The first thing that the AddEditBug.aspx page does when it loads is execute the following code in the

Page_Load event:

If Request.QueryString.Get(“ApplicationId”) IsNot Nothing Then Dim applicationId As Integer = _

Convert.ToInt32(Request.QueryString.Get(“ApplicationId”)) Dim applicationDescription As String = _

ListManager.GetApplicationDescription(applicationId) Helpers.SetApplicationSession(applicationId, applicationDescription)

End If

Helpers.CheckApplicationState ( _

Server.UrlEncode(Page.AppRelativeVirtualPath & “?” & _

Request.QueryString.ToString()))

If Request.QueryString.Get(“Id”) IsNot Nothing Then

bugId = Convert.ToInt32(Request.QueryString.Get(“Id”))

End If

If Not Page.IsPostBack Then

LoadData()

End If

417

Chapter 12

The first seven lines of code check if there is an ApplicationId on the query string. If there is one it switches to that application automatically. This is used in the Reporting page, described later in this chapter.

Then the application is validated. The AddEditBug page requires an active application stored in a session variable. If the variable isn’t present, the CheckApplicationState method redirects the user to the SwitchApplication page and passes along the URL of the current page so the user can be redirected back after an application has been chosen.

If there is also an Id on the query string, it’s converted to an Integer and stored in the private variable bugId. This bugId variable is later used in the code to determine the bug that must be retrieved from and stored in the database.

Finally, when the page is loading for the first time, all the controls are data-bound by calling

LoadData().

The LoadData() method starts off with binding the four drop-downs (lstFeature, lstFrequency, lstReproducibility, and lstSeverity) to their data sources. Each of these controls is bound to an Object DataSource control. These ObjectDataSource controls get their data by calling static methods in the ListManager class. Take a look at how the Frequency drop-down is bound to understand how this works. First, the page contains the following DataSource declaration:

<asp:ObjectDataSource ID=”odsFrequency” runat=”server” SelectMethod=”GetFrequencyItems” TypeName=”ListManager”>

</asp:ObjectDataSource>

The page also contains the following declaration for a drop-down:

<asp:DropDownList ID=”lstFrequency” runat=”server” AppendDataBoundItems=”True” DataSourceID=”odsFrequency” DataTextField=”Description” DataValueField=”Id” Width=”180px”>

<asp:ListItem Value=””>Please make a selection</asp:ListItem> </asp:DropDownList>

The drop-down is bound to the DataSource by setting its DataSourceID attribute. To ensure that the static “Please make a selection” list item remains present, AppendDataBoundItems is set to True.

When the drop-down is data-bound in the code-behind, the ObjectDataSource control’s DataBind method is invoked. The control then calls the GetFrequencyItems method located in the ListManager class. This method calls a private method called GetListItem and passes it an enumeration of ListType

.Frequency. The GetListItem method then gets the requested items from the database and stores them in the cache with a SqlCacheDependency attached to it. This ensures that the cached item is invalidated when the table used for the dependency is changed. The GetListItem method looks like this:

Private Shared Function GetListItems( _

ByVal myListType As ListType) As DataSet

Dim listItems As DataSet

Dim cacheKey As String = myListType.ToString() + “DataSet”

Dim tableName As String = myListType.ToString()

Dim SqlDep As SqlCacheDependency = Nothing

If HttpContext.Current.Cache(myListType.ToString() _

418

The Bug Base

+ “DataSet”) IsNot Nothing Then

listItems = CType(HttpContext.Current.Cache(cacheKey), DataSet) Else

‘ (Re)create the data and store it in the cache listItems = ListManagerDB.GetListItems(myListType)

Try

‘ Create a new SqlCacheDependency.

SqlDep = New SqlCacheDependency( _

AppConfiguration.DatabaseName, tableName)

Catch exDNEFNE As DatabaseNotEnabledForNotificationException

‘ Handle DatabaseNotEnabledForNotificationException Throw

Catch exTNEFNE As TableNotEnabledForNotificationException Throw

Finally

HttpContext.Current.Cache.Insert(cacheKey, listItems, SqlDep) End Try

End If

Return listItems

End Function

This method first tries to get the requested item from the cache. If it exists, it’s cast to a DataSet so it can be returned to the calling code. If the item no longer exists, it’s created by calling GetListItems in the ListManagerDB class and passing it the requested ListType. That method returns a DataSet that is stored in the cache using a SqlCacheDependency.

Before you can use SqlCacheDependencies in your application, you need to set up your database to support them. The database that comes with the Bug Base has already been set up for SQL cache invalidation, but if you’re using your own database, or need to enable caching on an existing database, use the following command from your ASP.NET 2.0 installation folder (located under %WinDir%\Microsoft.NET\ Framework):

aspnet_regsql.exe -S (local)\InstanceName -E -ed -d DatabaseName -et -t TableName

This registers the table you specify with TableName in the database DatabaseName. You can type aspnet _regsql.exe /? to get a help screen for this application.

The constructor for the SqlCacheDependency expects the name of the database you’re setting up the dependency against. Instead of hard-coding BugBase in the constructor method, there is a shared and public property in the AppConfiguration class that returns the name of the database. With that property, you can simply pass AppConfiguration.DatabaseName as the first argument to the constructor.

The constructor for the SqlCacheDependency class throws errors when either the database or the requested table hasn’t been set up for SQL caching. When an error is thrown, you simply rethrow it using the Throw keyword, so it will bubble up in the application to eventually cause an error that is caught by the Application_Error handler in the Global.asax file. If you don’t want to use SQL caching because you’re using a different database, you can simply remove the caching code from the GetListItems method. Alternatively, you can decide to store the data in the cache for a limited amount of time. This way you still have the benefits of caching, but you run the risk of working with stale data.

419

Chapter 12

The code for the GetListItems method in the ListManagerDB class is very similar to the code you saw earlier for the GetApplicationItems. The only thing that’s different is the way the name of the stored procedure is determined by looking at the ListType argument that is passed to this method:

Dim sql As String = “” Select Case theListType

Case ListType.Frequency

sql = “sprocFrequencySelectList” Case ListType.Reproducibility

sql = “sprocReproducibilitySelectList” Case ListType.Severity

sql = “sprocSeveritySelectList” Case ListType.Status

sql = “sprocStatusSelectList” Case Else

Throw New ArgumentException(“ListType must be a valid “ & _ “ListType enum. Current value is “ + theListType.ToString)

End Select

This process is repeated for each of the four drop-downs at the top of the page: lstFeature, lstReproducibility, lstFrequency, and lstSeverity.

With the four drop-downs bound to their data source the next step is to retrieve the bug from the database, but only when AddEditBug.aspx is in edit mode. Retrieval of a bug is done with the BugManager class:

Dim myBugManager As BugManager = New BugManager(Helpers.GetMemberId)

A new instance of the BugManager is created and the current member’s ID is passed to the constructor by calling Helpers.GetMemberId, which simply returns the session variable MemberId as a Guid. The MemberId is used for access rights checks in each of the BugManagerDB methods.

Dim myBug As Bug = myBugManager.GetBug(bugId)

The Bug object is retrieved by calling GetBug and passing it the ID of the requested bug. The GetBug method checks if a valid member ID has been passed and then delegates the responsibility of retrieving the bug from the database to the GetBug method in the BugManagerDB class. This method is similar to other methods in the data access layer when it comes to setting and opening the SQL connection. What’s different is that a SqlDataReader is used to hold the data instead of a DataSet. This DataReader is then used to fill the properties of the Bug object like this:

Using myReader As SqlDataReader = _ myCommand.ExecuteReader(CommandBehavior.CloseConnection)

If myReader.Read Then

theBug = New Bug(myReader.GetInt32(myReader.GetOrdinal(“Id”))) theBug.Title = myReader.GetString(myReader.GetOrdinal(“Title”)) ‘ ... other properties are set here

Else

theBug = Nothing End If myReader.Close()

End Using

420

The Bug Base

If the bug was found in the database, a new Bug object is created and then all of its public properties are set. Notice that GetOrdinal is used to retrieve a column’s index in the DataReader. This is because each of the Get* methods expects an Integer with the column’s position and not a string with the column name. Using GetOrdinal might make this code just a little slower, but it also makes it a lot more readable and flexible. Instead of knowing the exact location of a column in the result set, all you need to remember is the column’s name.

You pass the enumeration CommandBehavior.CloseConnection to the ExecuteReader method to ensure that the connection is closed when the reader is closed at the end of the Using block. This is good programming practice, because it explicitly closes the connection object, freeing up valuable resources.

Six of the properties of the Bug class are NameValue objects to expose both their internal ID and the userfriendly description. The NameValue objects are retrieved from the DataReader like this:

theBug.Status = New NameValue(myReader.GetInt32( _ myReader.GetOrdinal(“StatusId”)), _ myReader.GetString(myReader.GetOrdinal(“StatusDescription”)))

This code creates a new NameValue object, passes the ID and Name to the constructor of that class, and then assigns the object to the Bug object’s Status property. This allows you to access the property in your code like this, for example:

lblStatus.Text = theBug.Status.Name

When the bug is not found in the database, or the user doesn’t have enough rights to view it, Nothing is returned. Therefore, in the calling code back in AddEditBug.aspx you need to check if the object equals Nothing. If the bug is not Nothing, the bug’s properties are bound to the form controls:

If myBug IsNot Nothing Then

If User.IsInRole(“Developer”) OrElse User.IsInRole(“Manager”) Then If lstFeature.Items.FindByValue( _

myBug.Feature.Value.ToString()) IsNot Nothing Then lstFeature.Items.FindByValue(myBug.Feature.Value.ToString()).Selected = True

End If

‘ ... other controls are set here

This code executes only when the current user is in one of the required roles. If the user is a not a developer or a manager, she is not allowed to change any of the existing fields; static labels are shown instead, as in Figure 12-18.

Figure 12-18

Whereas a developer or a manager sees Figure 12-19.

421

Chapter 12

Figure 12-19

The rest of the code in this method is responsible for hiding or displaying the relevant controls on the page.

When the Save button is clicked, btnSave_Click is called and the page is validated by calling Page

.Validate(). When the page is completely valid, a new Bug object is created or an existing one is retrieved from the database using an instance of the BugManager:

Dim memberId As Guid = Helpers.GetMemberId()

Dim myBugManager As BugManager = New BugManager(memberId) Dim myBug As Bug

If bugId > 0 Then

myBug = myBugManager.GetBug(bugId)

Else

myBug = New Bug()

myBug.Application.Value = Helpers.GetApplicationId() myBug.CreateMemberId = memberId

End If

Next, each of the bug’s properties is retrieved from the form controls:

myBug.Title = txtTitle.Text

myBug.Feature.Value = Convert.ToInt32(lstFeature.SelectedValue) myBug.Frequency.Value = Convert.ToInt32(lstFrequency.SelectedValue) myBug.Priority = Convert.ToInt32(lstPriority.SelectedValue)

‘ ... other properties are set here

If bugId > 0 Then

‘ Only when we’re editing the bug, update the status field. myBug.Status.Value = Convert.ToInt32(lstStatus.SelectedValue)

End If

Notice that you only need to set the Value of each of the NameValue properties. The database only works with the internal IDs and doesn’t care about the “friendly descriptions” of these objects.

Once all the public properties have been set, the bug is saved by calling myBugManager.Insert UpdateBug(myBug) on the BugManager class. The InsertUpdateBug method passes the bug to a method with the same name in the data access layer that saves the bug in the database:

Public Shared Function InsertUpdateBug(ByVal theBug As Bug) As Integer Dim sql As String = “sprocBugInsertUpdateSingleItem”

Try

Using myConnection As New SqlConnection(AppConfiguration.ConnectionString)

Dim myCommand As SqlCommand = New SqlCommand(sql, myConnection) myCommand.CommandType = CommandType.StoredProcedure

If theBug.Id > 0 Then

422

The Bug Base

myCommand.Parameters.AddWithValue(“@id”, theBug.Id) End If

myCommand.Parameters.AddWithValue(“@title”, theBug.Title) myCommand.Parameters.AddWithValue(“@description”, theBug.Description) ‘ ... other properties are set here

myCommand.Parameters.AddWithValue(“@frequencyId”, theBug.Frequency.Value)

Dim myParam As SqlParameter = New SqlParameter myParam.Direction = ParameterDirection.ReturnValue myCommand.Parameters.Insert(0, myParam)

myConnection.Open()

myCommand.ExecuteNonQuery()

theBug.Id = CType(myParam.Value, Integer) myConnection.Close()

Return theBug.Id

End Using

Catch ex As Exception

Throw

End Try

End Function

When the Bug.Id is greater than zero, it is passed to the stored procedure by the AddWithValue method that creates a new parameter and sets the ID of the bug. Otherwise, the parameter remains null. The stored procedure knows that when the @id parameter is null it should insert a new bug item or update the item otherwise. Just as with the Id property, the code adds parameters for each of the public properties of the bug. At the end, an additional ReturnValue parameter is set up that retrieves the ID of the bug once it has been inserted or updated. With all the parameters set up, ExecuteNonQuery is called to save the bug in the database.

After the bug has been saved, the user is redirected back to the Bug List page, where the new bug appears at the top of the list. From this list, you can click the bug’s title to open the ViewBug page. This page displays a read-only version of the bug that is easy to print. The concepts used in this page are very similar to those in the AddEditBug page, without the additional complexity of hiding and displaying the relevant controls.

This concludes the process of inserting and updating bugs. The next step is to look at how you can retrieve bugs that have been filed from the database.

Searching and Viewing Bugs

When the number of bugs you have logged in the Bug Base grows, it becomes harder to manage them. The Bug List page for an application allows you to select active or inactive bugs, allowing you to focus on the open bugs. However, even that list of open bugs may grow quite long. And what if you wanted to find an older bug you know exists that has similar characteristics as a new bug you have found? With just the bug list pages, you’d be browsing through the list of bugs forever.

So to make it easier to find bugs, you need a good search tool. Fortunately, the Bug Base comes with a useful search tool. In fact, it comes with two search tools! On the main Bugs menu you find the Search Bugs item, which allows you to search for bugs in the current application. Under Reporting you find the

423

Chapter 12

Reports menu item that also allows you to search for bugs. Both search pages have a lot in common, but there are some important differences.

First of all, the Reports page is only accessible by members of the Manager group. If you’re not in that group, the menu item Reporting is not even visible. On the reporting page, you can search for bugs in all applications at the same time, whereas on the Search page your search is limited to the current application. This distinction is necessary to prevent testers or developers on one application from seeing bugs logged in an application they don’t have access to. Another difference is the possibility to search for a bug by its ID or a keyword on the search page. When searching for a bug, this is very useful because bugs are often referred to by their ID. On the reporting page, this option makes less sense. Usually, the purpose of the reporting page in a bug tracking tool is to get a list of bugs of a certain status, such as all open bugs. This allows a manager to quickly view the progress made in an application, or get a list of all bugs that still need work.

Despite the differences in functionality from a user’s point of view, these two pages work pretty much the same in terms of code. The next section dissects the Reports page and shows you how it works. Once you understand the Reports page you should have no trouble finding out what goes on in the Search page.

When you open the Reports page from the Reporting menu, you get the screen displayed in Figure 12-20.

Figure 12-20

424

The Bug Base

This form allows a user to set up a list of search criteria including the period the bug was filed, the application and its features, the person who filed the bug, and the severity, the status, and the priority. Once you choose an application from the Application drop-down, the page reloads to show you a list of features for the selected application. Except for the Application drop-down, you can select multiple options for all the other lists. Once you click the Report button, you get a list with the bugs that match your criteria, as shown in Figure 12-21.

Figure 12-21

If you want to change your search criteria, click the Change Search Criteria link at the top of the page. This reveals the form controls from Figure 12-20 again.

Take a look at the markup of Default.aspx in the Reports folder to see how this page works. Most of the concepts used in this page have already been used in other pages, such as AddEditBug. The page consists largely of controls that are bound to ObjectDataSource controls, which in turn are bound to methods in the business layer. A few things are different, though, and worth examining more closely. First of all, there’s the ObjectDataSource called odsMembers created with the following code:

<asp:ObjectDataSource ID=”odsMembers” runat=”server” SelectMethod=”GetAllUsers”

TypeName=”System.Web.Security.Membership”

>

425

Chapter 12

Instead of calling a method in the business layer of the Bug Base, this control is hooked up to the Membership provider and calls its GetAllUsers method. This method then returns a collection of

MembershipUser objects. A MemberhipUser has a ProviderKey and a UserName, the two fields that are used as the DataKeyField and DataValueField of the drop-down that displays the users:

<asp:ListBox ID=”lstMember” runat=”server” DataSourceID=”odsMembers” DataTextField=”UserName” DataValueField=”ProviderUserKey” AppendDataBoundItems=”True” SelectionMode=”Multiple”>

<asp:ListItem Value=”” Selected=”True”>[Don’t Filter]</asp:ListItem> </asp:ListBox>

Getting a list of users in a web page doesn’t get any easier than this!

The next piece of code you should look at is the code for the drop-down that displays the applications. The drop-down has its AutoPostBack property set to True, which means the page is posted back to the server whenever a new item is chosen in the drop-down. In the code-behind for the page you’ll find a method that fires whenever a postback occurs:

Protected Sub lstApplications_SelectedIndexChanged( _ ByVal sender As Object, ByVal e As System.EventArgs) _ Handles lstApplications.SelectedIndexChanged

lstFeature.Visible = True

lstFeature.Items.Clear()

lstFeature.Items.Insert(0, New ListItem(“[Don’t Filter]”, “”)) lstFeature.Items(0).Selected = True

End Sub

Inside this method, the Visible property of the Feature drop-down is set to True, and a new, static item is added to the list. By making the control visible, the ASP.NET run time knows that it now has to bind the control to its associated ObjectDataSource that looks like this:

<asp:ObjectDataSource ID=”odsFeature” runat=”server” SelectMethod=”GetFeatureItems” TypeName=”ListManager”>

<SelectParameters>

<asp:ControlParameter ControlID=”lstApplications”

DefaultValue=”-1” Name=”applicationId” PropertyName=”SelectedValue” Type=”Int32” />

</SelectParameters>

</asp:ObjectDataSource>

This ObjectDataSource control has a SelectParameter of type ControlParameter that looks at the

SelectedValue property of the Applications drop-down and passes it to the GetFeatureItems method. This method, placed in the business layer, only returns the features for the requested application.

The ObjectDataSource for the feature then fires its Selected event when it’s done retrieving the data. Inside this method for this event, the Feature drop-down is hidden when there are no items returned from the database:

Protected Sub odsFeature_Selected(ByVal sender As Object, ByVal e _ As System.Web.UI.WebControls.ObjectDataSourceStatusEventArgs) _

Handles odsFeature.Selected

Dim featureListVisible As Boolean = _

426