Common Scripting Topics

We will now delve into some useful and common scripting techniques and discuss topics that are germane to a wide range of scripts. This is not meant to be a comprehensive listthe function of the Beep script step should be fairly obvious to youbut rather these are the important areas to understand. They will help you establish a solid foundation in scripting.

For a complete reference to all the script steps in FileMaker 8, refer to our companion book, FileMaker 8 Functions and Scripts Desk Reference.

 

Error Management

Error management is an important part of the scripting process. Frequently scripts make assumptions about the presence of certain data or the existence of certain objects, or depend on a layout to establish context. If any of a given script's assumptions are not met, it either might not work or might produce unintended results. Error management involves identifying these assumptions and creating ways of dealing with them. You can bank on users finding odd, unpredictable ways to break your system. Applying some thought to how to manage such situations will serve you well in the long run.

Notice that FileMaker Pro 8 Advanced has the capability to enable and disable individual script steps. This facilitates testing significantly: You can turn off sections of your script that aren't finished and run discrete sections of your logic.

For further discussion of error handling, see "Handling Errors in Scripts," p. 512.

For more ideas on error management, see Chapter 17, "Debugging and Troubleshooting," p. 501.

 

Allow User Abort

Allow User Abort enables and disables a user's ability to press (-period) [Esc] to cancel a script in midstream. Generally speaking, it's the rare script that's designed to be cancelled gracefully at any time in its process. There's really no reason to ever turn Allow User Abort on, unless you're testing a loop script or some other long-running process. Any script that doesn't have Allow User Abort disabled allows users to cancel a script in progress, with consequences you may not intend.

Note that this is true for scripts that users are running, but the opposite is true for developers: If you're in the midst of writing a script and need to test a loop, for example, you should leave this setting turned on in order to halt your script if need be.

The other thing Allow User Abort does is take away the Cancel button when a script pauses, giving users only the option to continue. There are many cases in which canceling a script would leave the user stuck on a report layout or stranded midstream in some extended process.

To learn more about how to deal with incomplete script completion (atomicity in database lingo), refer to "Unfinished Scripts" in the "Troubleshooting" section at the end of this chapter.

 

Set Error Capture

The Set Error Capture script step either prevents or allows FileMaker's default error messages to be displayed to the user. When error capturing is off, FileMaker displays its own alert dialogs to the user if, for example, a record fails validation or a user runs a search without any find criteria. When error capture is turned on, the script in question captures errors and doesn't present them to the user. This allows you, the developer, to present your own, customized error messages, but imposes a greater burden in terms of checking for and managing errors yourself.

Handling errors well in scripts is a black art: It's difficult to always anticipate what errors will crop up. For more information on using the Set Error Capture script step, see Chapter 17, "Debugging and Troubleshooting," p. 501.

When doing your own error checking and managing, you'll want to use the Get ( LastError ) calculation function to programmatically deal with errors within your script. Use the If function to test Get ( LastError ) and present dialogs to the user as appropriate. Refer to FileMaker Pro's online help system for a list of error codes.

Be careful with the Set Error Capture script step. It certainly doesn't prevent errors from happeningit simply doesn't show the user a message about one that did. An error may happen, but the user's experience won't be interrupted to deal with it. This allows you to control how errors are managed within your script itself. You should not turn error capture on unless you have also added steps to identify and handle any errors that may arise.

To explore problems with error messages you think are being wrongly suppressed in scripts, refer to "Lost Error Messages in Scripts" in the "Troubleshooting" section at the end of this chapter.

Here's an example of a script segment that tests for an errorin this case a find request that results in zero found records:

Find_BirthdaysThisMonth
Enter Find Mode [ ]
Set Field [ Person::birthMonth[Month ( Get (CurrentDate) )] ]
If [ Get( LastError )  0 ]
Show Custom Dialog [ Title: "No Birthdays Found";
 Message: "There are no birthdays listed for this month.";
 Buttons: "OK" ]
End If

 

Setting and Controlling Data

Some of the primary uses of scripts lie in manipulating, moving, and creating data. Most of the script steps for manipulating field data are found in the Fields category.

Essentially, these Fields steps allow you to insert data into a given field programmatically, just as a user otherwise would. This can mean setting the field contents to the result of a calculation, copying the contents of one field into another, or simply inserting into a field whatever is on the user's clipboard.

As an example, imagine that you wanted to give users a button that would insert their name, the current date, and the current time into a comments field, and then place the cursor in the proper place for completing their comment:

# purpose: To insert user and date/time data into a comment field, preserving
# the existing information, and place the cursor in the correct position
# for the user to begin typing.
# dependencies: Need to be on the Main_Info layout, with the Comment field
# available. The script takes the user there.
# history: sl 2004 jan 25
#
#
Allow User Abort [ Off ]
Set Error Capture [ On ]
#
#
Go to Layout [ "Main_Info" (Movie) ]
#
# this next step applies the comment info in italics.
Set Field [ Movie::Comment; TextStyleAdd (
Movie::Comment & "¶¶" & Get ( AccountName ) & " " &
Get ( CurrentDate) & " " & Get ( CurrentTime);
Italic)
& "¶" ]
#
Go to Field [ Movie::Comment ]
Commit Records/Requests [ No dialog ]

Note

This script includes the full commenting approach described in this chapter and the two Allow User Abort and Set Error Capture steps. From here on out, we'll forego those details in the interest of brevity.

When using a Go to Field step, FileMaker Pro places the cursor at the end of whatever content already exists in the field, unless the Select/Perform option is enabled, in which case the entire field will be selected. If you wanted, you could use the Set Selection script step to place the cursor somewhere within the body of text.

Notice that the comment info is nested within a TextStyleAdd() function so that it will be displayed in italics.

For more information on calculation functions, including text formatting, see Chapters 8 and 14.

Set Field is by far the most used of the field category steps. The other functions in this category nearly all depend on the field in question being on the layout from which the script is being performed. You should get into the habit of using the Set Field command whenever possible, in preference to the others. It doesn't depend on a field being on a specific layoutor any layout, for that matterand it usually can accomplish what you're trying to do with one of the other steps.

You'll generally need the Insert script steps only when you expect user input. For example, you might place a button next to a field on a given layout called "index" that then calls up the index for a given field and waits until the user selects from its contents.

That script could often be a one-step script: Insert from Index (table::fieldname). As always, you'd use your template for clarity, but this script would open the index for a given field and wait for the user to select a value. Again, you should tend to think of scripts as evolutionary. Consider writing a script even for a one-step process because you might want to attach that script to multiple buttons or extend its operation in the future.

To manage cases in which your script seems to be affecting the wrong portal row or related record, refer to "Editing the Correct Related Records" in the "Troubleshooting" section at the end of this chapter.

For more discussion on indexes, see "Storage and Indexing," p. 86.

Caution

You might also discover the Copy, Cut, and Paste script steps. These work as you would expect. Copy and Cut place data onto the user's clipboard and Paste inserts from it. Cut and Copy overwrite anything already on the user's clipboard. Furthermore, Copy, Cut, and Paste depend on having access to the specified fields, and are therefore layout dependent. If, for some reason, you remove those fields from the specific layout in the future, your script will stop working. You should almost never use Copy and Paste for these reasons, and should defer instead to Set Field.

For further discussion of layout dependencies, as well as other types of dependencies that can get your scripts into trouble, see "Context Dependencies," p. 528.

Another example of using the Set Field script step concerns totaling child record data calculations and saving the results in a new record (presumably to track the growth of some quantity over time). Often a simple calculation field with a Sum (related field ) function works, but consider that with a large related data set, the performance of such calculations can become a problem. Furthermore, you cannot index that sort of a calculation fieldwhich might prove problematic for users performing find requests or for your needs as a developer. Consider instead creating a script to calculate and store your totals and calling that script only on demand:

StoreCurrentCustomerTotal
Go to Layout [ "Customer" (Customer) ]
Set Field [ Customer::storedTotal; Sum ( OrderbyCustomer::Amount ) ]
Set Field [ Customer::storedDate; Max ( OrderbyCustomer::Date ) ]
Commit Records/Requests
[ No dialog ]
Go to Layout [ original layout ]

The preceding script is a fairly typical example of drawing data from related records, of moving from layout to layout to establish proper context, and finally of using SetField to populate data.

Providing User Navigation

You might have noticed in FileMaker's Edit Script dialog a section of the script steps list devoted to navigation. One of the most common uses of scripts is to provide a navigation scheme to users whereby they can navigate from layout to layout, record to record, or window to window by using buttons or some other intuitive means.

There's not too much magic here: By using the Go to Layout script step, you'll get the fundamentals. Consider placing buttons along the top of each layout to offer a means of navigating to all user-facing layouts in your solution with a Go to Layout script attached.

By building complete navigation scripts, you can control the entire user experience of your solutions and can opt to close the Status Area if you want. Armed with find routines, sort buttons, reporting scripts, and a navigation interface, it is possible to build a complete application with a look and feel all its own.

Script Context and Internal Navigation

Consider that FileMaker uses layouts to determine script context: For any script step that depends on a specific table, you need to use Go to Layout steps to provide that context. Review the script we introduced at the beginning of the chapter:

Go to Layout [ "zdev_GlobalAdmin" (Globals) ]
Set Field [ Globals::gAccountName; Get (AccountName) ]
Set Field [ Current_User::LastLoginDate; Get (CurrentDate) ]
Set Field [ Globals::gUserNameDisplay; Current_User::Name_First ]
Set Field [ Globals::gUserMessage; "Welcome Back, "
 & Globals::gUserNameDisplay & "." ]
Go to Layout [ "Main Menu" (Globals) ]

This script takes itself to a zdev_GlobalAdmin layout, executes some steps (in this case sets data into fields), and then brings the user to a Main Menu layout. All the users see (presumably when they log in) is that they've landed on a Main Menu layout. They'll never see, or interact with, the zdev_GlobalAdmin layout, but the system will have done so. Had we written the script without the initial Go to Layout step, the routine would have had quite unexpected results.

Notice that the script makes use of a Current_User table occurrence. As related data, that information would likely be very different depending on the perspective from which a user viewed it. The purpose of navigating internally to a specific layout is to precisely control this context.

The point here is that you'll need to bring a script to a specific layout to establish a different context. Context is determined by the table occurrence associated with a given layout. The user might never see this internal navigation going on, but if you were to walk through the script step by step (for example, using the Script Debugger, covered in Chapter 17), you'd see the system go to the zdev_GlobalAdmin layout and then to the Main Menu layout.

Saved Script Options

Scripts tend to mirror the actions a user could perform manually but, obviously, do so without human intervention. It is possible in FileMaker to save find, sort, export, and other actions in a script (hard-coding them, if you will), or to prompt the user for some input to help perform these steps.

The advantages of hard-coding requests should be fairly obvious. If, for example, you need to prepare a report on active real estate listings, it makes sense to have one of your script steps be a Perform Find that returns all the records with a status of "active." The requirements of your report will rarely change, so you'll save users time (and possible errors) if you hard-code the find request.

On the other hand, allowing the user to provide input is a great way to make scripts more flexible. Continuing the example, you might create a real estate listings report and then in your script prompt the user for some search criteria. This can be done by either using a dialog that gives the user one or two choices (we'll cover that later in the chapter, in the "Working with Custom Dialogs" section), or simply allowing the report to act on the current found set and sort.

You will often find it helpful to build hard-coded find and sort routines. For example, you might want a script for finding overdue invoices, or easy-access buttons for sorting by first name, last name, or company.

FileMaker allows you to save complex find, sort, export, and import requests as necessary, and allows you to edit these requests within ScriptMaker.

Find Script Steps

FileMaker allows you to assemble and store complex find requests within scripts. In Figure 9.3, the script finds all overdue invoices over $500 and, in the same Perform Find step, omits invoice number 2004.1.1; the result replaces the found set.

Figure 9.3. Assemble as many find requests as necessary.

Notice that multiple requests have been added; this enables you to perform Or finds where you will be left with records that match either the first condition or the second.

A single find request is assembled via the Edit Find Request dialog (see Figure 9.4).

Figure 9.4. By adding multiple criteria to a single find request, you will be performing an And search.

Note in Figure 9.3, however, that we have opted to omit records that match the second request. Setting a request to omit records simply means that FileMaker will find those records that match the overall request and then take out or ignore those that meet the omit criteria. If you create a find request that does nothing but omit records, it replaces your existing found set with all records that don't match your request. (How's that for a double negative?) The following example shows a script that combines a find request with an omit request:

Find_Overdue_Invoices
Perform Find [ Specified Find Requests: Find Records;
 Criteria: Invoices::Total:"> 500"

 AND Invoices::DaysOverdue: "> 0"
Omit Records; Criteria: Invoices::Invoice_Number: "= '2004.1.1'" ]
 [ Restore ]

Other search-related script steps include Constrain Found Set and Extend Found Set. Just as though a user had chosen each command from FileMaker's menu-driven interface, Constrain reduces the current found set, eliminating any records that don't match the search criteria, and Extend adds those records from outside the set that match its criteria to the current found set.

Sort Script Step

Establishing saved sort orders in the Sort dialog works, happily, just as it does for users performing a manual sort (see Figure 9.5).

Figure 9.5. It's generally quite helpful to create sorting scripts for users. Sorting needs are usually fairly predictable and are always needed more than once.

One of the most common applications of sort scripts is in building column header buttons. Simply create a series of sort scripts and apply them to the buttons along the top of a list view (see Figure 9.6).

Figure 9.6. Scripting is often employed in creating more intuitive user interfaces for users.

Keep in mind that many of your reports depend on sorting, especially as you get into reporting by summary data. It's a good idea to create sort scripts for your reports and call them as subscripts, rather than hard-coding sort criteria into your report scripts themselves.

You may well create reports that behave differently depending on different sort ordersby setting up different, multiple subsummary parts on one report layout, for exampleso you'll want to factor the sorting logic into its own script or subscripts. A report with both a week-of-year subsummary part and a month subsummary part will display by week, by month, or by week and month depending on the sort options your script establishes. This is a handy technique for reducing the number of layouts you need in a systemwith a little bit of scripting you can use a single layout for three different reports.

For more on summary reporting, see Chapter 10, "Getting Started with Reporting," p. 273.

 

Using Conditional Logic

Another important element of scripting is the capability to branch scripts based on various conditions. To manage logically branching scripts, you use the If, Else, Else If, and End If script steps.

These conditional script steps work by performing a logical test, expressed as a calculation. If that calculation formula resolves to a true statement, FileMaker then executes all the script steps subordinate to (that is, nested within) an If or Else If statement.

One of the most common applications of conditional logic in FileMaker revolves around Perform Find script steps. Because we as developers can never guarantee the state of a given table's datain other words, how many records it containswe have to test for their existence in scripts that perform find requests and then branch accordingly if no records are found. Here's an example:

Find Overdue Orders
Set Error Capture [ On ]
Allow User Abort [ Off ]
#
Go to Layout [ "Order" (Order) ]
#
Perform Find [ Specified Find Requests:
 Find Records; Criteria: Order::Status: ""Overdue"" ]
[ Restore ]
If [ Get ( LastError ) = 401 ]
Show Custom Dialog [ Title: "Overdue Orders";
 Message: "There are no overdue orders in the system.";
 Buttons: "OK" ]
Show All Records
End If

In this simple script two outcomes are possible: Users will either be presented with a set of order records where their statuses have been set to Overdue or they will be presented with a dialog informing them that there are no overdue orders and will end with a full set of all orders.

The entire idea behind conditional logic is to allow the computer to determine which of multiple possible paths to take. Computers aren't terribly smart, so they make these decisions based entirely on Boolean (true/false) tests. At the end of every script step, FileMaker records an internal error that can be retrieved using the Get ( Last Error ) function. In the preceding script, if that function is storing a value of 401 then the nested steps within the If clause will be performed; otherwise, they won't be.

Figure 9.7 elaborates on the idea with a real-world example in which multiple branches are possible. At the conclusion of its script, one of three possible outcomes will have happened: A standard invoice will have (presumably) been printed, an overdue invoice will have been printed, or, if a stop work limit has been reached, a letter and review process will be initiated. The exact specifics here aren't important, but notice that the script results in one certain outcome (a standard invoice or an overdue invoice being printed) and then a second possible outcome (a letter and review occurring if a stop work condition also exists).

Figure 9.7. Notice that scripts within FileMaker's Script Editor are automatically indented. Liberal use of comments will help make scripts readable.

To learn how to bake error checking into your conditional tests, refer to "Conditional Error Defaults" in the "Troubleshooting" section at the end of this chapter.

There's no practical limit to the number of branches a script might take. For scripts of particular complexity, we recommend breaking them into subscripts (as the script in Figure 9.7 was) and, when necessary, creating a flowchart of the process before writing the script.

Using Loops

Another key scripting technique is looping. Looping allows you to execute a series of script steps over and over until some exit condition is met. This is very much the same as an If/Else If construct, but this time instead of performing a new branch of logic, you're simply telling the script to perform the same actions over again until a controlling conditional test returns a true value (for example, if the end of a found set is reached, or the results of a calculation come to some specific number).

A simple example of this might be stepping through each record in an invoice table's found set and generating a new invoice for any invoice that remains unpaid or that needs to be sent out again for some reason. The logic, without worrying about syntax, might look like this:

Go to first record in found set.
Begin Loop.
 Check whether the invoice is closed. (We'll assume
 unclosed invoices are those that need to be resent.)
 If CLOSED
 Go to next record.
 If there is no next record (you're at the end of
 your found set), then exit the loop.
 Else begin loop again (go to the "Begin Loop" step).
 If NOT CLOSED
 Close current invoice.
 Create/duplicate new invoice. (In a real system,
 there would likely be more steps involved here.)
 Go to next record.
 If there is no next record (you're at the end of
 your found set), then exit the loop.
 Else begin loop again (go to the "Begin Loop" step).
End Loop
Exit Script

Notice that an exit condition is established. The system tests in both If branches whether you're at the end of a found set and exits the script regardless of whether the last record in the set was closed.

Imagine you're a user doing this manually. You'd start at the top of a found set, use the book icon to page through each record one at a time, and then stop the process after you reach the end of your recordset.

Here's another example, this time in FileMaker's scripting syntax. It creates a series of new order records based on a request from the user (posted in a global field):

Set Field [ Globals::gCurrentCustomer ]
Show Custom Dialog [ Title: "New Order Items";
 Message: "How many Order Item rows do you want to create?";
 Buttons: "OK"; Input
#1: Globals::gNumNewOrderItems, "New Order Items to Create:" ]
Go to Layout [ "OrderbyCustomer" (OrderbyCustomer) ]
Loop
Exit Loop If [ Get (FoundCount) > Globals::gNumNewOrderItems ]
# note: new records in the OrderbyCustomer table auto-enter
# customer foreign key from the gCurrentCustomer field
New Record/Request
End Loop
Go to Layout [ original layout ]

This simple example demonstrates all the essential logic for working with loops. First, some condition by which the script exits the loop must be established. In this case a number provided by the user is used to exit the script. (Note that if this were a real-world script, we'd recommend some error-checking to make sure that the user input a positive integer.)

A loop exit condition is almost always useful if something changes during the course of a script. (It's possible you might be sitting in a loop, waiting for something elsewhere to change and checking periodically, but this kind of polling activity is not commonly needed in FileMaker Pro.)

Notice too where the commands are placed. The exit condition tests for a greater-than condition. This ensures that the script does, in fact, run the number of times a user requests. If the New Record/Request script step were placed above the Exit Loop If step, this script would still be perfectly valid, but it would generate one fewer new orders than the user requested.

Note too that if you're a perfectionist, it's possible to write the script like so:

Set Field [ Globals::gCurrentCustomer ]
Show Custom Dialog [ Title: "New Order Items";
 Message: "How many Order Item rows do you want to create?";
 Buttons: "OK"; Input
#1: Globals::gNumNewOrderItems, "New Order Items to Create:" ]
Go to Layout [ "OrderbyCustomer" (OrderbyCustomer) ]
Loop
New Record/Request
Exit Loop If [ Get (FoundCount) images/U2265.jpg border=0> Globals::gNumNewOrderItems ]
# note: new records in the OrderbyCustomer table auto-enter
# customer foreign key from the gCurrentCustomer field
End Loop
Go to Layout [ original layout ]

In this scenario you're saving one iteration through the loop by testing conditions immediately after the creation of a new record. It's unlikely that this makes much of a difference in this particular example, but when you write particularly large scripts, especially those that involve looping, we recommend you look for ways to make them perform as efficiently as possible. Speed and system performance should always be considerations when writing scripts.

Loops get more interesting when combined with conditional logic more complex than checking for an incremented counter. The first example for closed invoices did just this. It is possible to build a loop that tests for certain conditions within your system and exits only when those conditions are metfor example, a loop that processes all unclosed invoices, checking at the end of each cycle whether it has reached the end of a found set.

Loops can be exited in various ways. The simple conditional in the script example shown earlier is quite common. Another common technique is to use the Go to Record/Request/Page [Next, Exit After Last] script step. It enables you to step through a found set and exit a loop after the last record is reached.

Another way to exit a loop is to exit or halt the script altogether. You have two processes running: the script itself, which can be terminated, and the internal loop.

To cope with endless loop problems, refer to "Testing Loops" in the "Troubleshooting" section at the end of this chapter.

 

Working with Custom Dialogs

One of the most common user interactions necessary for a system is to capture a response to a question. "Are you sure you want to delete all records?" "Do you want to report on all records, or just your found set?" "Would you like fries with that?"

The Show Custom Dialog script step is a great, built-in way to capture this sort of interaction. (There are ways to create layouts that act and behave like dialog boxes, but they're a good bit more work.) Custom dialogs allow you to present some descriptive text or a question to a user and capture a response (see Figure 9.8).

Figure 9.8. Here's an example of a custom dialog. Notice that it has an appropriate title and specific, data-derived text.

To learn how to create pop-up layouts that behave as modal dialogs, see "Multiwindow Interfaces," p. 368.

Naturally, after you've created a custom dialog, you need to deal with the results. FileMaker Pro stores the user's button choice until the end of the current script or until you present another custom dialog. Think of these dialogs as existing solely within the space of a given script.

To identify which response the user chose to your dialog, use the Get ( LastMessageChoice ) function. This function returns a 1, 2, or 3 based on which button was clicked, from right to left. The rightmost button is identified as 1. The label you assigned to the button is inconsequential.

Conditional scripting similar to what was covered previously also allows you to test the choices a user made and respond accordingly. Here's an example:


 

[View full width]

Report_Revenue_Start Show Custom Dialog [ Title: "Revenue Report"; Message: "Do you want to view a Revenue Report by month, year, or a date range?"; Buttons: "Range", "Year", "Month"; Input #1: Invoices::Date_Range, "Date Range (e.g., 1/1 /2004...2/15/2004)" ] If [ Get ( LastMessageChoice ) = 1 ] Perform Script [ "__Report_DateRange" ] Else If [ Get ( LastMessageChoice ) = 2 ] Perform Script [ "__Report_YearSummary" ] Else If [ Get ( LastMessageChoice ) = 3 ] Perform Script [ "__Report_MonthSummary" ] End If

Custom dialogs are fairly flexible, but they do have limitations. The most obvious limitation is that their appearance cannot be altered. In a FileMaker layout you're able to apply images, background color fills, and other graphical attributes of the screen. Not so in a custom dialog. You are limited to a system-style dialog.

Second, and more important, if you provide input fields (as was shown in Figure 9.7), data entered is posted to your database only if the user clicks the first, rightmost button in the dialog. You do not have programmatic control of the input field behaviors. This then means that your users will have "post to database" as their default. Not optimal, but there you have it.

The third limitation of the dialog lies in scope: You're limited to three input fields and three buttons. If you need anything more complex, you have to use a standard FileMaker layout to build a custom pop-up layout.

Tip

One alternative to keep in mind is a range of plug-ins available that also offer dialogs. Visit FileMaker's website to explore those options.



Part I: Getting Started with FileMaker 8

FileMaker Overview

Using FileMaker Pro

Defining and Working with Fields

Working with Layouts

Part II: Developing Solutions with FileMaker

Relational Database Design

Working with Multiple Tables

Working with Relationships

Getting Started with Calculations

Getting Started with Scripting

Getting Started with Reporting

Part III: Developer Techniques

Developing for Multiuser Deployment

Implementing Security

Advanced Interface Techniques

Advanced Calculation Techniques

Advanced Scripting Techniques

Advanced Portal Techniques

Debugging and Troubleshooting

Converting Systems from Previous Versions of FileMaker Pro

Part IV: Data Integration and Publishing

Importing Data into FileMaker Pro

Exporting Data from FileMaker

Instant Web Publishing

FileMaker and Web Services

Custom Web Publishing

Part V: Deploying a FileMaker Solution

Deploying and Extending FileMaker

FileMaker Server and Server Advanced

FileMaker Mobile

Documenting Your FileMaker Solutions



Using FileMaker 8
Special Edition Using FileMaker 8
ISBN: 0789735121
EAN: 2147483647
Year: 2007
Pages: 296

Similar book on Amazon

Flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net