This chapter's code implements two features of the Library Project: (1) a simple helper class used with ListBox and ComboBox controls to manage text and data; and (2) a set of generic forms used to edit lookup tables in the Library, such as tables of status codes.
Supporting List and Combo Boxes
In Visual Basic 6.0 and earlier, ListBox and ComboBox controls included two primary array-like collections: List (used to store the display text for each item) and ItemData (used to store a 32-bit numeric value for each item). The List array was important to the user because it presented the text for each item. But many programmers depended more on the ItemData array, which allowed a unique identifier to be attached to each list item.
cboMonth.AddItem "January" cboMonth.ItemData(cboMonth.NewIndex) = 1 cboMonth.AddItem "February" cboMonth.ItemData(cboMonth.NewIndex) = 2 ... cboMonth.AddItem "December" cboMonth.ItemData(cboMonth.NewIndex) = 12
Later, after the user selected a value from the list, the numeric ID could be used for database lookup or any other designed purpose.
nMonth = cboMonth.ItemData(cboMonth.ListIndex)
The bad news is that neither List nor ItemData exists in the .NET variation of ListBox or ComboBox controls. The good news is that both are replaced with a much more flexible collection: Items. The Items collection stores any type of object you wantinstances of Integer, String, Date, Animal, Superhero, and you can mix them within a single ListBox. Because Items is just a collection of System.Object instances, you can put any type of object you wish in the collection. The ListBox (or ComboBox) uses this collection to display items in the list.
So, how does a ListBox control know how to display text for any mixture of objects? By default, the control calls the ToString method of the object. ToString is defined in System.Object, and you can override it in your own class. The ListBox control also includes a DisplayMember property that you can set to the field or property of your class that generates the proper text.
Let's see a ListBox in action. Add a new ListBox to a form, and then add the following code to the Form's Load event handler.
Public Class Form1 Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ListBox1.Items.Add(1) ListBox1.Items.Add("Easy") ListBox1.Items.Add(#5/3/2006#) End Sub End Class
Running this code displays the form in Figure 8-3.
Figure 8-3. A simple ListBox with three different items
For the old ItemData value, the ListBox control includes a ValueMember property that identifies the identifier field or property for the objects in the Items collection. But you don't have to use ValueMember. Instead, you can simply extract the object in question from the Items collection, and examine its members with your own custom code to determine its identity. In reality, it's a little more work than the old Visual Basic 6.0 method. But then again, because you can store objects of any size in the Items collection, you could opt to store entire database records, something you could never do before .NET.
Still, storing entire records in a ListBox or ComboBox control is pretty wasteful. It's usually much better to store just an ID number, and use it as a lookup into a database. That's what we'll do in the Library Project. To support this, we'll need to create a simple class that will expose a text and data value. First, let's go back into the Library code.
Load the "Chapter 8 (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 8 (After) Code" instead.
Let's put the class in a source code file all its own. Add a new class file through the Project Add Class menu command. Name the class ListItemData.vb and click the Add button. The following code appears automatically.
Public Class ListItemData End Class
This class will be pretty simple. It will include only members for text and item display. In case we forget to connect the text field to the ListBox or ComboBox's DisplayMember property, we'll also include an override to the ToString function, plus a custom constructor that makes initialization of the members easier. Add the following code to the body of the class.
Insert Chapter 8, Snippet Item 1.
Public ItemText As String Public ItemData As Integer Public Sub New(ByVal displayText As String, _ itemID As Integer) ' ----- Initialize the record. ItemText = displayText ItemData = itemID End Sub Public Overrides Function ToString() As String ' ----- Display the basic item text. Return ItemText End Function Public Overrides Function Equals(ByVal obj As Object) _ As Boolean ' ----- Allow IndexOf() and Contains() searches by ItemData. If (TypeOf obj Is Integer) Then Return CBool(CInt(obj) = ItemData) Else Return MyBase.Equals(obj) End If End Function
Later, when it's time to populate a ListBox, we can use this object to add the display and identification values.
ListBox1.Items.Add(New ListItemData("Item Text", 25))
The override of the Equals method allows us to quickly look up items already added to a ListBox (or similar) control using features already included in the control. The ListBox control's Items collection includes an IndexOf method that returns the position of a matching item. Normally, this method will only match the object itself; if you pass it a ListItemData instance, it will report whether that item is already in the ListBox. The updated Equals code will also return True if we pass an Integer value that matches a ListItemData.ItemData member for an item already in the list.
Dim itemPosition As Integer = SomeListBox.Items.IndexOf(5)
Editing Code Tables
Back in Chapter 4, "Designing the Database," when we designed the database for the Library Project, several of the tables were designed to fill simple ComboBox lists in the application. All of these tables begin with the prefix "Code," and contain records that rarely, if ever, change in the lifetime of the application. One such table is CodeCopyStatus, which identifies the current general condition of an item in the library's collections (see Table 8-1).
Because all of these tables have basically the same formatan ID field and one or more content fieldsit should be possible to design a generic template to use for editing these tables. A base (class) form would provide the basic editing features, to be developed in full through derived versions of the base form.
For the project, we will add two forms: a "summary" form (that displays a list of all currently defined codes) and a "detail" form (that allows editing of a single new or existing code). To make things even simpler, we will only include the most basic record-management functionality in the summary form. Most of the code needed to edit, display, and remove codes will appear in the detail forms.
The Generic Detail Form
Add a new form to the project (Project New Windows Form), naming it BaseCodeForm.vb. Alter the following properties as indicated.
Now access the source code for this class (View Code). The code will never create instances of this generic form directly, so let's disallow all direct instantiation by including the MustInherit keyword.
Public MustInherit Class BaseCodeForm End Class
The main features of the form will be the adding of new code records, the editing of existing code records, and the removal of existing records. Add three function skeletons that support these features. We could have made them MustOverride, but as you'll see later, we will want the option to keep the default functionality from the base generic form.
Insert Chapter 8, Snippet Item 2.
Public Overridable Function AddRecord() As Integer ' ----- Prompt to add a new record. Return the ID ' when added, or -1 if cancelled. Return -1 End Function Public Overridable Function DeleteRecord( _ ByVal recordID As Integer) As Boolean ' ----- Prompt the user to delete a record. ' Return True on delete. Return False End Function Public Overridable Function EditRecord( _ ByVal recordID As Integer) As Integer ' ----- Prompt the user to edit the record. Return the ' record's ID if saved, or -1 on cancel. Return -1 End Function
The detail form will take responsibility for filling the ListBox control on the summary form with its items. Two methods will handle this: one that adds all items, and one that updates a single item. The derived class will be required to supply these features.
Insert Chapter 8, Snippet Item 3.
' ----- Fill a ListBox control with existing records. Public MustOverride Sub FillListWithRecords( _ ByRef destList As ListBox, ByRef exceededMatches As Boolean) ' ----- Return the formatted name of a single record. Public MustOverride Function FormatRecordName( _ ByVal recordID As Integer) As String
The detail form must also display the proper titles and usage information on the summary form.
Insert Chapter 8, Snippet Item 4.
' ----- Return a description of this editor. Public MustOverride Function GetEditDescription() As String ' ----- Return the title-bar text for this editor. Public MustOverride Function GetEditTitle() As String
While most of the tables will supply a short list of alphabetized codes, some tables will include a large number (possibly thousands) of codes. The summary form will support a search method, to locate an existing code quickly. Because only certain derived forms will use this feature, we won't include MustOverride.
Insert Chapter 8, Snippet Item 5.
Public Overridable Sub SearchForRecord( _ ByRef destList As ListBox, _ ByRef exceededMatches As Boolean) ' ----- Prompt the user to search for a record. Return End Sub
Finally, the detail form will indicate which of the available features can be used from the summary form. The summary form will call each of the following functions, and then enable or disable features as requested.
Insert Chapter 8, Snippet Item 6.
Public Overridable Function CanUserAdd() As Boolean ' ----- Check the security of current user to see ' if adding is allowed. Return False End Function Public Overridable Function CanUserEdit() As Boolean ' ----- Check the security of this current to see ' if editing is allowed. Return False End Function Public Overridable Function CanUserDelete() As Boolean ' ----- Check the security of this current to see ' if deleting is allowed. Return False End Function Public Overridable Function UsesSearch() As Boolean ' ----- Does this editor support searching? Return False End Function
That's it for the generic detail form. Later on in the book, we'll create derived versions for each of the code tables.
Generic Summary Form
The summary form is a little more straightforward, because it is just a plain form. When it starts up, it uses an instance of one of the derived detail forms to control the experience presented to the user. I've already added the form to the project; it's called ListEditRecords.vb, and looks like Figure 8-4.
Figure 8-4. The Generic Summary form
A large ListBox control fills most of the form, a control that will hold all existing items. There are also buttons to add, edit, delete, and search for items in the list. There's a lot of code to manage these items; I've already written it in a code snippet. Switch to the form's source code view, and add the source code just after the "Public Class ListEditRecords" line.
Insert Chapter 8, Snippet Item 7.
The first line of the added code defines a private instance of the generic detail form we just designed.
Private DetailEditor As Library.BaseCodeForm
This field holds an instance of a class derived from BaseCodeForm. That assignment appears in the public method ManageRecords.
Public Sub ManageRecords(ByRef UseDetail _ As Library.BaseCodeForm) ' ----- Set up the form for use with this code set. Dim exceededMatches As Boolean DetailEditor = UseDetail RecordsTitle.Text = DetailEditor.GetEditTitle() RecordsInfo.Text = DetailEditor.GetEditDescription() Me.Text = DetailEditor.GetEditTitle() ActAdd.Visible = DetailEditor.CanUserAdd() ActEdit.Visible = DetailEditor.CanUserEdit() ActDelete.Visible = DetailEditor.CanUserDelete() ActLookup.Visible = DetailEditor.UsesSearch() DetailEditor.FillListWithRecords(RecordsList, _ exceededMatches) RefreshItemCount(exceededMatches) Me.ShowDialog() End Sub
The code that calls ManageRecords passes in form instance, one of the forms derived from BaseCodeForm. Once assigned to the internal DetailEditor field, the code uses the public features of that instance to configure the display elements on the summary form. For instance, the detail form's CanUserAdd function, a Boolean value, sets the Visible property of the ActAdd button. The FillListWithRecords method call populates the summary ListBox control with any existing code values. After some more display adjustments, the Me.ShowDialog method displays the summary form to the user.
Although the user will interact with the controls on the summary form, most of these controls defer their processing to the detail form, DetailEditor. For example, a click on the "Add" button defers most of the logic to the detail form's AddRecord method. The code in the summary form doesn't do much more than update its own display fields.
Private Sub ActAdd_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles ActAdd.Click ' ----- Let the user add a record. Dim newID As Integer Dim newPosition As Integer ' ----- Prompt the user. newID = DetailEditor.AddRecord() If (newID = -1) Then Return ' ----- Add this record to the list. newPosition = RecordsList.Items.Add( _ (New Library.ListItemData( _ DetailEditor.FormatRecordName(newID), newID))) RecordsList.SelectedIndex = newPosition RefreshButtons() RefreshItemCount(False) End Sub
Most of the remaining code in the summary form is either just like this (for edit, delete, and search features), or is used to refresh the display based on user interaction with the form. Be sure to examine the code to get a good understanding of how the code works. In later chapters, when adding actual detail forms, you'll see this code in action.