Feb. 15, 2011, 4 p.m.
posted by soulmaker
This chapter's project will add a lot of code to the Library Program, as much as 25 percent of the full code base. Most of it is identical to code we added in earlier chapters, so I won't print it all here. There's a lot to read here, too, so I won't overload you with pasting code snippets right and left. But as you add each new form to the project, be sure to look over its code to become familiar with its inner workings.
Load the "Chapter 12 (Before) Code" project, either through the New Project templates, or by accessing the project directly from the installation directory. To see the code in its final form, load "Chapter 12 (After) Code" instead.
Overloading a Conversion
Operator overloading is new with Visual Basic's 2005 release. When I originally designed the Library Project using Visual Basic 2003, operator overloading was unknown in the language, and I had to make due with the features I had available. Looking back, it's not easy to say, "This section of code would work 100 times better through operator overloading." There just isn't much code like that in a typical business application that shuttles around SQL statements and related data.
But enough of my whining. Operator overloading is a useful tool, and I have grown especially fond of the CType overload. Let's add a CType overload to a class we first designed back in Chapter 8, "Classes and Inheritance": ListItemData. This class exposes both ItemText and ItemData properties, providing access to the textual and numeric aspects of the class content. Its primary purpose is to support the tracking of ID numbers in ListBox and ComboBox controls. If we need to know the ID number of a selected item in a ListBox control (let's name it SomeList), we use code similar to the following.
Dim recordID As Integer = _ CType(SomeList.SelectedItem, ListItemData).ItemData
There's nothing wrong with that code. But I thought, "Wouldn't it be nice to convert the ListItemData instance to an Integer using the CInt function, and not have to mess with member variables like ItemData?"
Dim recordID As Integer = _ CInt(CType(SomeList.SelectedItem, ListItemData))
Hmm. The code's not that different. But, hey, why not? Let's do it. To support this conversion, we need to add a CType overload to the ListItemData class. Open that class's file, and add the following code as a member of the class.
Insert Chapter 12, Snippet Item 1.
Public Shared Widening Operator CType( _ ByVal sourceItem As ListItemData) As Integer ' ----- To convert to integer, simply extract the ' integer element. Return sourceItem.ItemData End Operator
That's pretty simple. This widening conversion from ListItemData to Integer just returns the Integer portion of the instance. There are only about four or five places in the current Library Project that directly access the ItemData member, and it's not that important to go back and change them. But we'll use this conversion overload frequently in the new code added in this chapter.
Global Support Features
We need to add a few more global variables and common global routines to support various features used through the application. Two new global variables will track settings stored in the database's SystemValue table. Add them as members to the General module (in General.vb).
Insert Chapter 12, Snippet Item 2.
Public DefaultItemLocation As Integer Public SearchMatchLimit As Integer
The Library program identifies books and other items as stored in multiple locations, such as multiple branches or storage rooms. DefaultItemLocation indicates which one of these locations, from the CodeLocation table, is the default. The "DefaultLocation" entry of the SystemValue database table stores this value permanently.
When searching for books, authors, or other things that could result in thousands of matches, the SearchMatchLimit indicates the maximum number of matches returned by such searches. It's stored as the "SearchLimit" system value.
Because we're already in the General module, add three more helper functions.
Insert Chapter 12, Snippet Item 3.
Record Editors and Supporting Forms
Now things really start to hop. We'll add 23 new forms to the application in this chapter. Most of them implement basic code editors, similar to the UserName.vb and GroupName.vb files we built in Chapter 11, "Security." Other forms exist to provide additional support for these record editors. I won't reprint anything I've gone over before, but I'll point out some interesting new code on our way through each of these 23 forms.
If you're following along in the "Before" version of this chapter's project, you will need to enable each form as you encounter it. To do this, select the file in the Solution Explorer window, and change the file's Build Action property (in the Properties panel) from "None" to "Compile."
The first four forms allow the librarian to limit the information overload that comes through using a database with thousands of books, publishers, and authors. You probably remember that the generic ListEditRecords form displays all existing records from a table of records by default. This works fine for the security groups stored in the GroupName table since you probably won't have even a dozen of those. But listing all books in even a small library can generate quite an imposing list. And depending on the speed of your workstation, it can take a while to load all book titles into the list.
The four "search-limiting" forms help reduce the number of records appearing in the list at once. When the librarian accesses the list of books and other library items, the ItemLimit form (see Figure) provides a quick search prompt that reduces the listed results.
The ItemLimit form acts like a bar-room bouncer for items
The form lets the user retrieve all records, or specific items based on item name (with wildcard support). Once the matches are loaded, the user can access this form again by clicking on the Lookup button on the ListEditRecords form for those types of code editors that support lookups (authors, items, patrons, and publishers).
We are ready to include these four search-limiting forms in the project.
Keyword and Subject Editors
While most record editors provide a full editing experience through the ListEditRecords form, some are subordinate to other editor forms. Keywords and subjects are a good example. Although they each have their own independent tables (Keyword and Subject), I chose to allow editing of them through the form that edits individual library items, the NamedItem form (added later). That form manages all interactions between the Keyword and Subject records and the NamedItem table, all through the intermediate many-to-many tables ItemKeyword and ItemSubject.
The KeywordAdd and SubjectAdd forms provide a simple text entry form for a single keyword or subject. Include each of these forms now into the project.
More Named Item Support Forms
As we'll see later, the NamedItem form is one of the most complex forms added to the Library Project so far. It manages everything about a generalized library item (like a book). Each item can have multiple copies, authors, keywords, subjects, and so on. It's simply too much editing power to include on a single form. We already added two of the subordinate forms: KeywordAdd and SubjectAdd. Let's add five additional support forms.
Although this code does not inherit from BaseCodeForm as other record editors do, it still has many of the features of those forms, including a SaveFormData routine that writes records to the database.
One interesting thing that this form does have is support for reading barcodes. Many barcode readers act as a "wedge," inserting the text of a scanned barcode into the keyboard input stream of the computer. Any program monitoring for barcodes simply has to monitor normal text input.
Barcode wedge scanners append a carriage return (the Enter key) to the end of the transmitted barcode. This lets a program detect the end of the barcode number. But in most of the Library program's forms, the Enter key triggers the OK button and closes the form. We don't want that to happen here. To prevent this, we'll add some code to this form that disables the auto-click on the OK button whenever the insertion point is in the Barcode text entry field.
Insert Chapter 12, Snippet Item 4.
Private Sub RecordBarcode_Enter( _ ByVal sender As Object, ByVal e As System.EventArgs) _ Handles RecordBarcode.Enter ' ----- Highlight the entire text. RecordBarcode.SelectAll() ' ----- Don't allow Enter to close the form. Me.AcceptButton = Nothing End Sub Private Sub RecordBarcode_Leave( _ ByVal sender As Object, ByVal e As System.EventArgs) _ Handles RecordBarcode.Leave ' ----- Allow Enter to close the form again. Me.AcceptButton = ActOK End Sub Private Sub RecordBarcode_KeyPress(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) _ Handles RecordBarcode.KeyPress ' ----- Ignore the enter key. If (e.KeyChar = ChrW(Keys.Return)) Then e.Handled = True End Sub
Inherited Code Editors
Twelve of the forms added in this chapter inherit directly from the BaseCodeForm class. Add them to the project as I review each one.
Well, that's 11 of the 12 derived forms. The last one is the NamedItem form, shown here in Figure.
The NamedItem form with the General tab active
The NamedItem form is the largest and most complex of the forms that derive from BaseCodeForm. It edits primary library items recorded in the NamedItem database table. It's complex because it also directly or indirectly manages records in other subordinate tables: ItemAuthor, ItemCopy, ItemKeyword, ItemSubject, and indirectly Author, Keyword, Publisher, and Subject.
All of the fields on the General and Classification tabs are basic data entry fields that flow directly into the NamedItem table, just as is done with the other record-editing forms. The Publisher and Series fields use separate selection forms (PublisherAddLocate and SeriesAddLocate) to obtain the ID values stored in NamedItem. Here's the code that looks up the publisher.
' ----- Prompt the user. newPublisher = (New PublisherAddLocate).PromptUser() If (newPublisher = -1) Then Return ' ----- Check to clear the publisher. If (newPublisher = -2) Then RecordPublisher.Text = "Not Available" PublisherID = -1 Return End If
The other four tabsAuthors/Names, Subjects, Keywords, and Copiesmanage subordinate records. The code is pretty consistent between the four different tabs, so I'll limit my comments to the Authors/Names tab (see Figure).
The NamedItem form with the Authors/Names tab active
The controls on this tab are quite similar to those on the ListEditRecords form; they exist to manage a set of records in a table. In this case, it's the ItemAuthor table. For the presentation list, I chose to use a ListView control instead of a standard ListBox control. By setting a ListView controls's View property to "Details," setting its FullRowSelect field to True, and modifying its Columns collection (see Figure), you can quickly turn it into a multi-column list box.
The ColumnHeader editor for a ListView control
When you add an item to this list, you also have to add "sub-items" to have anything appear in all but the first column.
Dim newItem As Windows.Forms.ListViewItem = _ AuthorsList.Items.Add("John Smith") newItem.SubItems.Add("Illustrator")
The Add button brings up the AuthorAddLocate form, while the Properties button displays the ItemAuthorEdit form instead.
Before any of the subordinate records can be added, the "parent" record must exist in the database. That's because the "child" records include the ID number of the parent record, and without a parent record, there is no parent ID number. If you look in each of the Add button routines on this form, you will find code like this.
' ----- The record must be saved first. If (ActiveID = -1) Then ' ----- Confirm with the user. If (MsgBox("The item must be saved to the database " & _ "before authors or names can be added. Would you " & _ "like to save the record now?", _ MsgBoxStyle.YesNo Or MsgBoxStyle.Question, _ ProgramTitle) <> MsgBoxResult.Yes) Then Return ' ----- Verify and save the data. If (ValidateFormData() = False) Then Return If (SaveFormData() = False) Then Return End If
If this is a brand-new NamedItem record (ActiveID = -1), this code will save it before allowing the user to add the subordinate record. Any invalid data that prevents the record from being saved will be caught in the call to ValidateFormData.
Actually, the calls to both ValidateFormData and SaveFormData are the same ones that occur when the user clicks on the OK button. Normally, that triggers a return of the new record's ID number to the calling form. But what if SaveFormData gets called by adding an author, but then the user clicks the Cancel button (which normally returns a -1 value to indicate "no record added")? To avoid that, the SaveFormData function sets a class-level variable named SessionSaved.
SessionSaved = True
This flag is cleared when the form first opens, but is set to True pretty much any time a subordinate record changes. The NamedItem form's overridden AddRecord and EditRecord functions check for this flag before returning to the calling form.
If (Me.DialogResult = Windows.Forms.DialogResult.OK) Or _ (SessionSaved = True) Then Return ActiveID Else Return -1
There's lots of other interesting code in the NamedItem form. But at nearly 1,400 lines (not counting the related designer code), I'll have to let you investigate it on your own.
Connecting the Editors to the Main Form
OK, take a breath. That was a lot of code to go through. But if you run the program now, you won't see any difference at all. We still need to connect all of the record editors to the main form. They all connect through the LinkLabel controls on the main form's Administration panel (PanelAdmin). We need to add 12 LinkClicked event handlers to access all of the new and various forms. Go ahead and add them now to the MainForm class.
Insert Chapter 12, Snippet Item 7.
Private Sub AdminLinkPublishers_LinkClicked( _ ByVal sender As System.Object, ByVal e As _ System.Windows.Forms.LinkLabelLinkClickedEventArgs) _ Handles AdminLinkPublishers.LinkClicked ' ----- Make sure the user is allowed to do this. If (SecurityProfile(LibrarySecurity.ManagePublishers) = _ False) Then MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Exclamation, ProgramTitle) Return End If ' ----- Let the user edit the list of publishers. ListEditRecords.ManageRecords(New Library.Publisher) ListEditRecords = Nothing End Sub
After doing a quick security check, the code calls up the standard ListEditRecords form, passing it an instance of the record editor it is to use.
There are still a few inactive links on the Administration panel that we'll enable in later chapters.
Settings the Default Location
The program is now ready to run with all of its new features in place. Because we added only administrative features, you must click the Login button in the upper-right corner of the main form before gaining access to the Administration panel and its features. Unless you changed it, your login user name is "admin" with no password.
Although you can now run the program and access all of the record editors, you won't be able to add new item copies until you set a default location. To set the location:
Alternatively, you can update the "DefaultLocation" record in the SystemValue table directly using SQL Server Management Studio Express. If the ID of the location to use is 1, use this SQL statement to make the change.
UPDATE SystemValue SET ValueData = '1' WHERE ValueName = 'DefaultLocation'
In a future chapter, we'll add a more user-friendly method to update this default location.
Speaking of user friendly, we're about to enter the not-user-friendly but logic-friendly world of text structured data: XML.