As advertised, this chapter's project focuses on the printing of check-out and fine-payment receipts. But we'll also add all of the code that lets patrons and librarians check in and check out books and other library items.
Load the "Chapter 19 (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 19 (After) Code" instead.
Supporting Raw Printing
In the interest of frank and honest discussion, I must tell you that I didn't come up with the basic code for raw printing in this section. Oh, some of the code is mine, both stylistically and imaginatively. But I didn't figure out all of the links between the application and the winspool.drv file. That code originally came from Microsoft Knowledge Base article number 322090, which describes raw printing support from .NET applications. It uses a feature of .NET known as "interop" that allows .NET code to "interoperate" with older unmanaged COM-based components and applications.
Boy, am I glad that I got that off my chest. I mean, if anyone thought I was the one who came up with the code you are about to see, there would be angry mobs storming my house nightly, and general turmoil in the streets. The code, contained in the RawPrinterHelper class, is just plain ugly. Well, there's no sense postponing it any longer. Create a new class named RawPrinterHelper.vb, and use the following code for its definition.
Insert Chapter 19, Snippet Item 1.
Imports System.Runtime.InteropServices Public Class RawPrinterHelper ' ----- The code in this class is based on Microsoft ' knowledge base article number 322090. ' Web: http://support.microsoft.com/?id=322090 ' ----- Structure and API declarations. <StructLayout(LayoutKind.Sequential, _ CharSet:=CharSet.Unicode)> _ Private Structure DOCINFOW <MarshalAs(UnmanagedType.LPWStr)> _ Public pDocName As String <MarshalAs(UnmanagedType.LPWStr)> _ Public pOutputFile As String <MarshalAs(UnmanagedType.LPWStr)> _ Public pDataType As String End Structure <DllImport("winspool.Drv", EntryPoint:="OpenPrinterW", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function OpenPrinter(ByVal src As String, _ ByRef hPrinter As IntPtr, ByVal pd As Long) As Boolean End Function <DllImport("winspool.Drv", EntryPoint:="ClosePrinter", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function ClosePrinter( _ ByVal hPrinter As IntPtr) As Boolean End Function <DllImport("winspool.Drv", EntryPoint:="StartDocPrinterW", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function StartDocPrinter( _ ByVal hPrinter As IntPtr, ByVal level As Int32, _ ByRef pDI As DOCINFOW) As Boolean End Function <DllImport("winspool.Drv", EntryPoint:="EndDocPrinter", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function EndDocPrinter( _ ByVal hPrinter As IntPtr) As Boolean End Function <DllImport("winspool.Drv", EntryPoint:="StartPagePrinter", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function StartPagePrinter( _ ByVal hPrinter As IntPtr) As Boolean End Function <DllImport("winspool.Drv", EntryPoint:="EndPagePrinter", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function EndPagePrinter( _ ByVal hPrinter As IntPtr) As Boolean End Function <DllImport("winspool.Drv", EntryPoint:="WritePrinter", _ SetLastError:=True, CharSet:=CharSet.Unicode, _ ExactSpelling:=True, _ CallingConvention:=CallingConvention.StdCall)> _ Private Shared Function WritePrinter( _ ByVal hPrinter As IntPtr, ByVal pBytes As IntPtr, _ ByVal dwCount As Int32, ByRef dwWritten As Int32) _ As Boolean End Function Public Shared Function SendStringToPrinter( _ ByVal targetPrinter As String, _ ByVal stringContent As String, _ ByVal documentTitle As String) As Boolean ' ----- Send an array of bytes to a printer queue. ' Return True on success. Dim printerHandle As IntPtr Dim errorCode As Int32 Dim docDetail As DOCINFOW = Nothing Dim bytesWritten As Int32 Dim printSuccess As Boolean Dim contentBytes As IntPtr Dim contentSize As Int32 On Error Resume Next ' ----- Set up the identity of this document. With docDetail .pDocName = documentTitle .pDataType = "RAW" End With ' ----- Convert the string to ANSI text. contentSize = stringContent.Length() contentBytes = Marshal.StringToCoTaskMemAnsi( _ stringContent) ' ----- Open the printer and print the document. printSuccess = False If OpenPrinter(targetPrinter, printerHandle, 0) Then If StartDocPrinter(printerHandle, 1, docDetail) Then If StartPagePrinter(printerHandle) Then ' ----- Send the content to the printer. printSuccess = WritePrinter(printerHandle, _ contentBytes, contentSize, bytesWritten) EndPagePrinter(printerHandle) End If EndDocPrinter(printerHandle) End If ClosePrinter(printerHandle) End If ' ----- GetLastError may provide information on the ' last error. For now, just ignore it. If (printSuccess = False) Then errorCode = _ Marshal.GetLastWin32Error() ' ----- Free up unused memory. Marshal.FreeCoTaskMem(contentBytes) ' ----- Complete. Return printSuccess End Function End Class
Although ugly, the code is relatively clear-cut. The SendStringToPrinter method prepares a string for printing by forcing it to a standard ANSI format. It then uses the functions in the winspool.drv library to open a new print job, and send the prepared content to it. There's a whole lot of "marshalling" going on in the code through members of the Marshall class. Because winspool.drv is an unmanaged library, all data calls must be shuttled indirectly between the managed Library application and the unmanaged winspool.drv library.
Now that we have a convenient class that will send any raw content to any specific printer, let's add some code to use it. First, we need to add a helper class for a portion of the ticket printing. Create a new class file named CheckedOutItem.vb, and replace its empty class template with the following code.
Insert Chapter 19, Snippet Item 2.
Public Class CheckedOutItem ' ----- Used to store the details of each checked out ' on the main form, although it also supports ' receipt printing. Public ItemTitle As String Public CopyNumber As Integer Public Barcode As String Public DueDate As Date End Class
We'll use this class to convey the details to be printed on the receipt when checking out items. Speaking of ticket printing, let's add the class that does the actual printing. Create a new module file (not a class) named TicketPrinting.vb. Replace its empty module definition with the snippet code.
Insert Chapter 19, Snippet Item 3.
The code includes three methods that drive printing: PrintCheckoutTicket, PrintBalanceTicket, and PrintPaymentTicket. These methods are called from other parts of the application when it's time to present a printed ticket to the user. The TicketPrinting module also includes a few other methods that support these three primary methods. Because these three methods are somewhat similar in structure, let's just look at PrintCheckoutTicket.
Public Sub PrintCheckoutTicket(ByVal patronID As Integer, _ ByVal checkedOutItems As ListBox) ' ----- Print out a ticket of what the patron checked ' out. The supplied ListBox control contains ' objects of type CheckedOutItem. Dim ticketWidth As Integer Dim ticketText As System.Text.StringBuilder Dim counter As Integer Dim patronFines As Decimal Dim itemDetail As CheckedOutItem On Error GoTo ErrorHandler ' ----- Ignore if there is nothing to print. If (patronID = -1) Or (checkedOutItems.Items.Count = 0) _ Then Return ' ----- Get the width of the ticket. ticketWidth = My.Settings.ReceiptWidth If (ticketWidth <= 0) Then ticketWidth = 40 ' ----- Build the heading. ticketText = GetTicketHeader(patronID, ticketWidth) If (ticketText Is Nothing) Then Return ' ----- Process each checked-out item. For counter = 0 To checkedOutItems.Items.Count - 1 ' ----- Extract the detail from the list. itemDetail = CType(checkedOutItems.Items(counter), _ CheckedOutItem) ' ----- Add the item name. ticketText.AppendLine(Left(itemDetail.ItemTitle, _ ticketWidth)) ' ----- Add the barcode number and due date. ticketText.AppendLine(LeftAndRightText( _ itemDetail.Barcode, "Due: " & _ Format(itemDetail.DueDate, "MMM d, yyyy"), _ ticketWidth)) ticketText.AppendLine() Next counter ' ----- If there are fines due, print them here. patronFines = CalculatePatronFines(patronID) If (patronFines > 0@) Then ticketText.AppendLine("Fines Due: " & _ Format(patronFines, "$#,##0.00")) ticketText.AppendLine() End If ' ----- Add the bottom display text. ticketText.Append(GetTicketFooter(ticketWidth)) ' ----- Send the ticket to the printer. RawPrinterHelper.SendStringToPrinter( _ My.Settings.ReceiptPrinter, _ ticketText.ToString(), "Checkout Receipt") Return ErrorHandler: GeneralError("TicketPrinting.PrintCheckoutTicket", _ Err.GetException()) Return End Sub
The code builds a string (actually a StringBuilder) of display content, adding details about each checked-out item to a string buffer. Then it calls SendStringToPrinter to send the content to the configured receipt printer (My.Settings.ReceiptPrinter).
We'll add the code that calls PrintCheckoutTicket later. Right now, let's add code that calls the two other methods. When the Payment Record form closes, we want to automatically print a receipt of all payments made while the form was open. Add the following code to the PatronRecord.ActClose_Click event handler, just before the code already found in that handler.
Insert Chapter 19, Snippet Item 4.
' ----- Print out a ticket if needed. If (SessionPayments.Count > 0) Then _ PrintPaymentTicket(ActivePatronID, SessionPayments) SessionPayments.Clear() SessionPayments = Nothing
Then, add some code to the ActBalanceTicket_Click event handler, also in the PatronRecord class, that prints a balance ticket when the user requests it.
Insert Chapter 19, Snippet Item 5.
' ----- Print a ticket of all balances. PrintBalanceTicket(ActivePatronID, Fines)
The Library Project prints three types of barcodes: (1) item barcodes that you can stick on books, CDs, and anything else that can be checked-out or managed by the system; (2) patron barcodes that can be made into patron identification cards; and (3) miscellaneous barcodes that a library can use for any other purpose. All three barcode types are printed through the new BarcodePrint form. Figure 19-9 shows the controls included on this form.
Figure 19-9. One form, three barcode types, many happy labels
I've already added this form to the project, including its code. Here's the code for the Preview button, which should look familiar after I beat its concepts into you throughout this chapter.
Private Sub ActPreview_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles ActPreview.Click ' ----- The user wants to preview the labels. On Error Resume Next ' ----- Make sure the user supplied valid data. If (VerifyFields() = False) Then Return ' ----- Load in all of the page-specific details to be ' used in printing. If (LoadPageDetails() = False) Then Return ' ----- Create the preview dialog. Me.Cursor = Windows.Forms.Cursors.WaitCursor PageSoFar = 0 PreviewMode = True BarcodeDoc = New System.Drawing.Printing.PrintDocument ' ----- Display the preview. BarcodePreview.Document = BarcodeDoc BarcodePreview.ShowDialog() BarcodeDoc = Nothing Me.Cursor = Windows.Forms.Cursors.Default End Sub
The Print button's code is almost exactly the same, but it uses a PrintDialog instance instead of PrintPreviewDialog. It also keeps track of the final printed barcode number so that it can help avoid overlaps the next time they are printed.
The BarcodeDoc_PrintPage event handler does the actual barcode printing. Its code combines the BarcodeLabel.PreviewArea_Paint and BarcodePage.PreviewArea_Paint event handlers into one glorious printing machine.
To enable use of the barcode printing form, add the following statements to the ActReportsBarcode_Click event handler in the MainForm class.
Insert Chapter 19, Snippet Item 6.
' ----- Make sure the user is allowed to do this. If (SecurityProfile(LibrarySecurity. _ ManageBarcodeTemplates) = False) Then MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Exclamation, ProgramTitle) Return End If ' ----- Show the barcode label printing form. Call (New BarcodePrint).ShowDialog()
Renewal of Checked-Out Patron Items
For a library patron, the only thing more important than checking out and in items is being able to read those items. The Library Project won't help anyone with that, but it will do that check-in, check-out transaction thing through the code we add in this chapter. Let's start by adding the renewal code for currently checked-out items. The Renew button on the Patron Record form initiates the process. Add the code to the PatronRecord.ActRenewItemsOut_Click event handler that does the renewal.
Insert Chapter 19, Snippet Item 7.
The code does some calculations to determine the new due date (avoiding holidays), and then updates the database in a transaction.
TransactionBegin() ' ----- Update the record. sqlText = "UPDATE PatronCopy SET DueDate = " & _ DBDate(dueDate) & ", Renewal = " & renewsSoFar & _ " WHERE ID = " & itemDetail.DetailID ExecuteSQL(sqlText) ' ----- Update the patron record. sqlText = "UPDATE Patron SET LastActivity = Now " & _ "WHERE ID = " & ActivePatronID ExecuteSQL(sqlText) TransactionCommit()
Support for Check-In and Check-Out
If a library adds barcode labels to all of its items, then check-in and check-out will be via a barcode reader. But a very small library using the program may not have the staff time available to barcode everything on the shelves. Therefore, the Library Project needs to support check-in and check-out by title. During check-out or check-in, the user enters either a barcode or a title (partial or complete). Non-numeric entries are assumed to be titles, and initiate a title search. The new CheckLookup.vb form, pictured in Figure 19-10, displays all matches for the entered title.
Figure 19-10. A title matching form for both check-in and check-out
Although the fields on the form initially indicate that they are for check-out only, the form does double duty, altering its visage for check-in purposes. Additionally, check-in listings are limited to only those items already checked out.
I've already added this form to the project, along with its source code. Most of the code queries the database for matching library items and displays the results using an owner draw list box. It is a subset of the code found in the ItemLookup.vb form. The only real difference between check-in and check-out occurs in the PerformLookup method. This code block starts to build the main item selection SQL command, and then ends it with these statements.
If (asCheckIn) Then sqlText &= " AND IC.ID IN" _ Else sqlText &= " AND IC.ID NOT IN" sqlText &= " (SELECT ItemCopy FROM PatronCopy " & _ "WHERE Returned = 0)"
So the difference is "IN" versus "NOT IN."
The CheckItemByTitle function is the main interface to the form's logic.
Public Function CheckItemByTitle(ByVal CheckIn As Boolean, _ ByVal searchText As String) As Integer
You pass this function the user-supplied title (searchText) and a flag indicating check-in or check-out, and it returns the ItemCopy.ID database field for the selected library item.
All of the remaining changes in this chapter occur in the MainForm class, so let's go there now. The UpdateDisplayForUser method adjusts the main form's features when an administrator logs in or out. One feature we didn't take into account before is the administrator-defined ability for patrons to check out their own items without librarian assistance. To support that feature, we need to change some of the code in the UpdateDisplayForUser method. About ten lines into the code, in the conditional section that sets up the display for patrons, you'll find these four lines.
LabelTasks.Visible = False LineTasks.Visible = False PicCheckOut.Visible = False ActCheckOut.Visible = False
Replace these four lines with the following code.
Insert Chapter 19, Snippet Item 8.
' ----- See if patrons can check out items by themselves. Dim userCanCheckOut As Boolean = _ CBool(Val(GetSystemValue("PatronCheckOut"))) LabelTasks.Visible = userCanCheckOut LineTasks.Visible = userCanCheckOut PicCheckOut.Visible = userCanCheckOut ActCheckOut.Visible = userCanCheckOut
We also need to add similar security-related code to the TaskCheckOut method. Here are the first few lines of code from that method.
' ----- Update the display. AllPanelsInvisible() If (SecurityProfile(LibrarySecurity.CheckOutItems)) Then _ PanelCheckOut.Visible = True
Replace these lines with the following code.
Insert Chapter 19, Snippet Item 9.
' ----- Check Out mode. Dim userCanCheckOut As Boolean ' ----- See if patrons can check out items by themselves. userCanCheckOut = CBool(Val(GetSystemValue("PatronCheckOut"))) ' ----- Update the display. AllPanelsInvisible() If (userCanCheckOut Or _ SecurityProfile(LibrarySecurity.CheckOutItems)) Then _ PanelCheckOut.Visible = True
The actual check-out of items occurs on the main form itself. First, a patron is identified, and then the items to check out get processed. Let's add a class-level variable to MainForm that keeps track of the patron. And as long as we're adding definitions, we'll also add two constants that refer to images stored in the MainForm.StatusImages control. These constants will be used in some check-in-related code added a little later. Add the following code to the start of the class definition.
Insert Chapter 19, Snippet Item 10.
Private ActiveCheckOutPatron As Integer = -1 Private Const StatusImageBad As Integer = 0 Private Const StatusImageGood As Integer = 1
When the user identifies the patron to use for check-out, and then starts checking items out, the last step is a click of the Finish button, indicating the end of the check-out process for that patron. (Skip ahead to Figure 19-11 if you want to see the Finish button now.) However, there is nothing to stop the user from jumping to another part of the program, or from exiting the program completely, without first pushing the Finish button. We must anticipate this rude behavior so typical of software users. To ensure that check-out completes properly, we will add some code to three places in MainForm that should catch any such discourteous actions by the user. Add the following code to the start of these three methods: (1) the MainForm_FormClosing event handler; (2) the ShowLoginForm method; and (3) the AllPanelsInvisible method.
Figure 19-11. The check-out panel on the main form
Insert Chapter 19, Snippet Item 11 three times.
' ----- Finish the in-process check-out if needed. If (ActiveCheckOutPatron <> -1) Then _ ActFinishCheckOut.PerformClick()
Checking Out Items
All of the check-out code (except for the code in the CheckLookup.vb form) appears in the main form's class. Check-out is one of the eight main display panels accessed through this form (see Figure 19-11).
Here's the process for checking out items from the check-out panel.
Let's add the code for each of these three buttons. First, add code to the ActCheckOutPatron_Click event handler.
Insert Chapter 19, Snippet Item 12.
This code prompts the user for patron selection, and displays the remaining fields if successful. Here's the part of the code that does the prompting.
' ----- Get the ID of the patron. patronID = (New PatronAccess).SelectPatron() If (patronID = -1) Then Return ' ----- Get the patron name. sqlText = "SELECT FirstName + ' ' + LastName FROM Patron " & _ "WHERE ID = " & patronID patronName = CStr(ExecuteSQLReturn(sqlText)) ' ----- Is this patron active? sqlText = "SELECT Active FROM Patron WHERE ID = " & patronID If (CBool(ExecuteSQLReturn(sqlText)) = False) Then MsgBox("Patron '" & patronName & _ "' is marked as inactive.", MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Exclamation, ProgramTitle) Return End If
Add code to the ActDoCheckOut_Click event handler, which processes each item through the Check Out button.
Insert Chapter 19, Snippet Item 13.
As I mentioned before, this code differentiates between numeric entry (barcodes) and other entries (titles).
If (IsNumeric(Trim(CheckOutBarcode.Text))) Then ' ----- Probably a barcode supplied. Get the related ID. sqlText = "SELECT ID FROM ItemCopy WHERE Barcode = " & _ DBText(Trim(CheckOutBarcode.Text)) copyID = DBGetInteger(ExecuteSQLReturn(sqlText)) If (copyID = 0) Then ' ----- Invalid barcode. MsgBox("Barcode not found.", MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Exclamation, ProgramTitle) CheckOutBarcode.Focus() CheckOutBarcode.SelectAll() Return End If Else ' ----- Look up by title. copyID = (New CheckLookup).CheckItemByTitle(False, _ Trim(CheckOutBarcode.Text)) If (copyID = -1) Then Return End If
Eventually, after verifying that the item is available for patron use, the code checks out the item by updating the relevant records in the database.
TransactionBegin() ' ----- Update patron copy record. sqlText = "INSERT INTO PatronCopy (Patron, ItemCopy, " & _ "CheckOut, Renewal, DueDate, Returned, Missing, " & _ "Fine, Paid) VALUES (" & ActiveCheckOutPatron & ", " & _ copyID & ", " & DBDate(Today) & ", 0, " & _ DBDate(untilDate) & ", 0, 0, 0, 0)" ExecuteSQL(sqlText) ' ----- Update the patron record. sqlText = "UPDATE Patron SET LastActivity = GETDATE() " & _ "WHERE ID = " & ActiveCheckOutPatron ExecuteSQL(sqlText) TransactionCommit()
The last of the three buttons is the Finish button. Add code to the ActFinishCheckOut_Click event handler.
Insert Chapter 19, Snippet Item 14.
This code simply resets the display fields in preparation for the next patron check-out.
The list box on the check-out panel needs to display two columns of data: (1) the due date; and (2) details of the item such as title and barcode. These values were added to the list using the CheckedOutItem class we added a little earlier in the chapter. Add code to the CheckedOutItems_DrawItem event handler.
Insert Chapter 19, Snippet Item 15.
Checking In Items
Checking in items is much simpler, because we don't need to first identify the patron. The barcode or title of the check-in item is sufficient to complete all processing. Figure 19-12 shows the check-in panel.
Figure 19-12. The check-in panel on the main form
This panel includes a date indicating when the item will be checked in. Normally, that's today, but if library items are turned in through a nighttime repository after business hours, the librarian might want to adjust the date to "Yesterday," just in case any of these items were turned in before midnight. Let's add some code so that the panel indicates "Today" or "Yesterday" or some other day when a date changes. Add the following code to the CheckedInDate_ValueChanged event handler.
Insert Chapter 19, Snippet Item 16.
' ----- Adjust the day in the display. Select Case DateDiff(DateInterval.Day, _ CheckInDate.Value, Today) Case 0 ' ----- Today CheckInDay.Text = "Today" CheckInDay.BackColor = SystemColors.Control CheckInDay.ForeColor = SystemColors.ControlText Case 1 ' ----- Yesterday CheckInDay.Text = "Yesterday" CheckInDay.BackColor = Color.Red CheckInDay.ForeColor = Color.White Case Else ' ----- X days ago CheckInDay.Text = DateDiff(DateInterval.Day, _ CheckInDate.Value, Today) & " days ago" CheckInDay.BackColor = Color.Red CheckInDay.ForeColor = Color.White End Select
The actual check-in occurs when the user enters a barcode or title in the text field, and clicks the Check In button. Add code to the ActDoCheckIn_Click event handler.
Insert Chapter 19, Snippet Item 17.
After doing some lookups and confirmation checks, the code checks in the item through database updates.
' ----- Do the check-in in a transaction. TransactionBegin() ' ----- Update patron copy record. sqlText = "UPDATE PatronCopy SET CheckIn = " & _ DBDate(CheckInDate.Value) & _ ", Returned = 1 WHERE ID = " & patronCopyID ExecuteSQL(sqlText) ' ----- Update the patron record. sqlText = "UPDATE Patron SET LastActivity = " & _ "GETDATE() WHERE ID = " & patronID ExecuteSQL(sqlText) TransactionCommit()
That's it for the check-in and check-out procedures, and all ticket printing. It's pretty good code, but not yet perfect. What we haven't yet added is code to properly process fines on items before they are checked in, or as they are adjusted in other ways. We will postpone this logic until Chapter 21, "Licensing Your Application." Until then, let's look at another application printing feature: reports.