Free!  FoxPro to WPF screen converter
   Almost free!  One day of FREE VFP to WPF evaluation of your FoxPro app ONSITE
   Free!  Read the foreword to Les' new book (available here)...
Skip Navigation Links

Using ChildFormDialogs

StrataFrame has a mechanism to handle parent-child relationships that you're going to love


Les Pinter

Login or register to download source code

Coding time: 30 minutes.

 Jump to:   Setting up the project   Adding Business Objects   Data Retrieval Methods   Building the Forms   Conclusion 

     


Introduction

How many times do we need to relate the records in one table to the selected record in another table? That seems to be the most common task in many applications. It can take a lot of code, and it's almost the same code, over and over and over again. It's about time someone encapsulated all of these requirements into a component.

StrataFrame has a component called the ChildFormDialog that takes care of these behaviors, and thus greatly reduces the amount of coding that you have to do. Specifically, it passed data to the business object in a child form, calls the form, then evaluates the result code (e.g. "Ok" or "Cancel") to determine whether or not to save any changes, additions or deletions.

When you need to manage child records for a selected parent record. you simply create your forms, dropping the relevant business object on each form. You establish a Parent-Child Relationship between the two business objects. You then develop the user interface, usually based on ListViews. Finally, you drop a ChildFormDialog component on the calling form, specifying the child form to call and the correspondence between business objects in the called and calling forms.

A quick preview of the ChildFormDialog

Create a StrataFrame project, then add a SF Standard Form called frmChildForm. From the StrataFrame toolbox, drop a ChildFormDialog and a Button on Form1. Select the ChildFormDialog, right-click to open the Properties Sheet, and set the ChildForm property to frmChildForm. Then, add this code to the Click event of the Button on Form1:

   childFormDialog1.ShowDialog()

Press F5 to run the form, and click the button on Form1, and frmChildForm is displayed. So the ChildFormDialog's main purpose is to display the child form. But if that's all it did, there wouldn't be a reason to use it. However, if you reopen the Properties Sheet for the ChildFormDialog object, you'll notice that there's a Business Object Translations combobox in the middle of the property editor. This is where you tell the ChildFormDialog that there are tables involved, and that you want it to to pass data from one to the other.

This is where the hidden magic takes place. It has the effect of isolating child record changes as a transaction. You've been led down the path of dealing with child records the right way. Sorry for the subterfuge; it was necessary in order to save you from all of the headaches that you've had by trying not to do so.

Another nice feature is that the ChildFormDialog.ShowDialog() method returns a DialogResult enum (Ok, Cancel, etc). SF Standard Forms also have a DialogResult property. If you set the DialogResult of a form that was called by a ChildFormDialog, the called form's DialogResult is passed back through the ChildFormDialog, and can be tested when you return from showing the child form.

<rant>That's what is meant by a framework. It gives you machinery for doing things the right way, so that you don't pay the price for not having seen the consequences of taking the wrong path. StrataFrame is full of these, and that's why I got hooked.</rant>

Before you can do that, however, you'll need to add parent and child business objects and describe their relationship. That's what the rest of this article is about.

The demonstration case

The demonstration case for this article is somewhat involved, because in order to document what the ChildFormDialog does for you, all aspects of the interaction between the forms need to be described. We'll make it, as Einstein said, "as simple as possible, but no simpler." The good news is that this demonstration is a real-world application of this important technology, and can be cloned in your own applications with just a few changes.

For our demonstration, we'll use the Customers and CustomerNotes tables from the StrataFrameSample database. We'll use three forms:

  1. A ListView in the parent form to let the user select a customer; in addition to the customer name, phone and email, we'll include a column indicating how many notes exist for each customer. There will also be a button to launch a child form showing the notes for the selected customer, and another to requery the ListView in case other users have made changes;
  2. A child form of the parent form, with a ListView containing the subject column of any existing CustomerNotes records, as well as buttons to Add, Edit or Delete customer notes, and Save and Cancel buttons to finalize any child record actions; and
  3. A third form, in turn a child form of the second form, with textboxes bound to the two main columns in the Customer Notes table (Subject and Note), and with Save and Cancel buttons.

To launch the child form, we'll load the child records related to the selected parent record into the child business object, and then call the ChildFormDialog's ShowDialog() method, which in turn shows the child form. The ChildFormDialog's ShowDialog() method returns a DialogResult value, which is picked up from the child form's DialogResult field (it's not a property, so it doesn't show in the form's Properties sheet) which is set in code in the Save and Cancel buttons of the child form itself.

Upon returning from the child form, if Save was clicked in the child form, you save the changes to the child records; if Cancel was clicked in the child form, you Undo the changes. This allows you to either save or abandon changes on all child records affected while the child form was displayed. You can add, edit and delete records at will. They'll be dealt with as a batch when you return.

Loading the child form records into their business object just before calling the form is the secret to this simplified handling. It makes abandoning changes to child records particularly easy, as you can imagine. Thanks to the ParentRelationship declared on the child business object, we can use the child BO's FillByParentPrimaryKey method to load the child records related to the selected parent record. The child form gets a copy of the child records that were loaded just before it was launched. For the sake of efficiency, the ListView in the child form uses a special method called CopyDataFrom that's built into all SF business objects to load its data from the child business object.

So, while you can expect a few new concepts in what you're about to see, the end result, as always, is less code and fewer things that can get overlooked. By cloning and modifying these three screens, you will be able to deal with important subassemblies of your own applications with essentially about 40 lines of code (that you actually have to write). Let's get started.

First, you'll need to create a StrataFrame project. If you're familiar with setting up a StrataFrame project and adding business objects, you can skip the next 2 sections; just add a CustomersBO and a CustomerNotesBO SF Business Object, configure them using the corresponding tables in the StrataFrameSample database, and jump straight to Data Retrieval Methods,


Setting up the project
 Top 

As always, we start with a StrataFrame Windows Application project. You'll have to choose the language, but in this article we'll include the code for both C# and VB. The starting project appears in Fig. 1, below:


My design for this project consists of three forms:

  1. A parent form (frmParentForm) with a ListView to display customers;
  2. a child form (frmNotesList) with a ListView to display the subject field of CustomerNotes records for the selected customer; and
  3. an Add/Edit form (frmNoteEditor) to display a single CustomerNotes record's Subject and Note fields in textboxes.

The parent form will have a button to show related CustomerNotes; the frmNotesList form will have buttons for Add, Edit, Delete a note and to Save or Cancel changes; and the frmNoteEditor form will have Save and Cancel buttons.

First, rename Form1 by right-clicking on Form1 and selecting Rename and changing the name to frmParentForm (Fig. 2)


To add the two child forms, right-click on the project and add an SF Standard Form, naming it frmNotesList, then add another one and name it frmNoteEditor. Press F6 to rebuild the project. Your project in the Solution Explorer will look like Fig. 3:


We'll flesh out these forms shortly. But first, the data.


Adding the business objects
 Top 

StrataFrame uses Business Objects (table classes) that it generates directly from your datasource. We'll use the Customers table from the StrataFrameSample database that installs with StrataFrame. Right-click on your project and select Add, New Item, then pick SF Business Object from the available items. Be sure to change the name to CustomersBO before you click Add (Fig. 4)


Repeat the process, naming the second object CustomerNotesBO. The Solution Explorer will reflect the two added files. (Fig. 5):


However, at this point they don't really contain any code that is specific to any of your tables; they're just empty classes.

To add the table-specific code, you use the StrataFrame Business Object Mapper (the first menuitem in the StrataFrame menu pad). The BOM is a utility that reads the structure of the data source, and then generates (among other things) one public property for each column in the underlying table in the selected business object class.

This is what allows you to use the notation Customers.Address1, which has no intrinsic meaning in .NET unless you have an object named Customers with a public property named Address1.

This seemingly obvious fact has caused shock and awe in the FoxPro community, because the notation Customer.Address1 is simply built into FoxPro from the start. The fact that .NET requires reams of code to produce this trivial notation leaves FoxPro developers stunned. How could a language that deals with data not have a built-in notation for referring to the columns in a table. Bad news is, no, it doesn't; good news is, StrataFrame writes the code for the "business object" (which may consist of several thousand lines of code) in about two seconds. And you don't even have to look at the code. It just runs like a fox.

However, when they're initially generated, business objects don't even know what table they're related to. There's an app for that.

Click on Business Object Mapper from the StrataFrame menu pad in the IDE menu. You'll see the treeview in Fig. 6. The little red "x" over the Project name and the business objects below it indicate that they're not yet "configured" (i.e. associated with a database and table).

To do this, select the project name from the Available Projects tree and click on the Configure Project link, which becomes enabled when you select the project name.


We don't yet have one, so in the Choose Project dialog, click the New Button. Then, from the Project Properties screen that appears (Fig. 7), fill in a project name and description. Leave the Database Deployment Toolkit radiobutton selection in the Default Structure Settings group as it is; this is an external file used in the IDE where StrataFrame stores project information. (You can also store it in a database, but we generally won't do so.) Be sure to select the new project (on the Choose Project dialog page) after you create it.


You should, however, build and store your SQL connection string information by selecting the little button with the three dots just to the right of the SQL Server Connection string textbox in the Default Connection Strings group (circled in Fig. 7, above) to bring up the Database Connection Wizard (Fig. 8)


Once you've configured the project, highlight CustomersBO and click on the Configure Business Object link (See Fig. 6) to produce the screen shown in Fig. 9:


Now select the SQL Server radio button and click the Select Source button: You'll be able to select the StrataFrameSample database and pick the Customers table (Fig. 10).


The result is a display of the columns in the table that will be represented by properties in the resulting business object (Fig. 11):


Note: The tables in Fig. 10 whose names begin with SFS are components of the StrataFrame Security system, which can easily be added to any application. Our article 414 shows how to do it.

Configure CustomerNotesBO next, selecting the CustomerNotes table from the StrataFrameSample database.

When you close the BO generation dialog screen, you'll see that the little red "x"s have disappeared. There are other useful things that can be done in the BOM, but for the moment we don't need to do anything else. Select Rebuild All, and close the BOM dialog.

Establishing the Parent-child relationship in the data

The way that StrataFrame business objects manage parent-child relationships is by means of a complex property on the child BO called ParentRelationship. In the Solution Explorer, double-click on the CustomerNotesBO, then right-click anywhere except on the text that ends in "click here to switch to code view" and select Properties. Click on the little button with the three dots to the right of the ParentRelationship property and make the selections shown in Fig. 12:


You're done creating business objects. Unless you change the structure of one of your tables, you probably won't open the BOM again during this project.


Data Retrieval Methods
 Top 

(Note: BE SURE to set the ParentRelationship property of the CustomerNotesBO object before you proceed.)

Ever since I can remember, it's been considered best practices to relegate SQL statements to your business objects and thereby keep SQL out of your presentation code ("separating the interface from the implementation"). The way you do that in StrataFrame is by putting them in the business object.

You might think that a Business Object knows how to return data; it doesn't. You usually have to write a line or two of code. For the CustomerBO object, we'll want to return, say, 100 records (The StrataFrameSample database Customers table has over 19,000 customers in it, and we don't want to load them all into a ListView). To do that, we'll write a method that in turn calls a method contained in all SF business objects - FillDataTable. It takes a single parameter: A SQL SELECT statement. It doesn't need to return the records; FillDataTable stores them in the business object.

To add our FillTop100 method for CustomerBO, double-click on CustomersBO in the Solution Explorer and select Click here to switch to code view. Expand the Data Retrieval Methods region of CustomersBO and add this (Listing 1):



The only custom code we'll need for the CustomerNotesBO object is a little Int32 method to return the number of records in the CustomerNotes table for each customer. This appears as the leftmost column on frmParentForm's ListView to indicate how many notes each customer has. The one time we need to load child records from CustomerNotes, we'll actually call one of the many built-in methods: FillByParentPrimaryKey(). Double-click on the CustomerNotesBO object, select View Code, and type in the code shown in Listing 2:



Note that I used a different syntax in the VB version than in the C# version of the CustomerNotesBO scalar method GetNoteCount(); both ways work fine. I'm not much for methodological purity.

Just a note on business object syntax: As we've alluded to, multiple records are loaded into a business object. However, CustomersBO1.City displays only one city. This is because business objects have an index that defaults to the Current Record, unless you specifically use an index to view a different one. So even if you've loaded 1000 records, if you're currently parked on record number 3, if you display CustomersBO1.City, you'll see CustomersBO1.Rows(2).Items("City").

Anyway, we're done with the business objects. Rebuild the project, and you're ready to build the child forms.


Building the forms
 Top 

We're going to build three forms in this demo:

  1. A parent form with a ListView showing the name, phone and email from the Customers table, plus a column to show how many notes exist for each customer, and a button to launch a child form showing the subject line of related notes;
  2. A child form with a ListView showing notes related to the selected Customers record, with add, edit, delete, save and cancel buttons; and
  3. A second child form issuing from the first child form that drills down to the selected customer note for adding or editing a note.

To create them, change the name of Form1 to frmParentForm and add two SF Standard Forms named frmNotesList and frmNoteEditor.

Important! You need to place all objects on all forms before you try to type in the code; a reference to Form3.ChildFormDialog1 will fail if Form3 doesn't yet have a ChildDialogForm1. And ChildDialogForms can't be configured to point to their business object until it's been added to the form. So add everything to all forms first, then configure objects and type in the code.

ListView population in StrataFrame

Before we dive into the details of the forms, let's talk for a minute about how StrataFrame ListViews are populated. A SF ListView has its own internal datatable. You can pass in a datatable, but you can also use any number of other ways to load it. In parent-child scenarios, we like to copy the child records to the child business object just before the child form is launched. The ListView of the child form can then simply copy the records we just stored in the child business object into the listview's internal table. Since the ChildFormDialog's BusinessObjectTranslation copies the records from the calling form's business object to the child form business object, the child table data is isolated, and saving or abandoning changes is simplified.

There are two steps involved in populating a ListView in StrataFrame:

  1. Declaring the Columns collection; and
  2. Defining the PopulationDataSource

The Columns collection is very straightforward: How many columns, how wide, and with what headings. You can set up even a complex ListView's columns in a minute or two. But that's not where the work takes place.

As you'll discover below, clicking on the PopulationDataSource property of a SFListView displays a fairly complicated dialog. You have to select four things:

  1. Which business object to use;
  2. Which data retrieval method to execute; this may be one that you wrote in your business object, but it's more likely to be one that's already included in your generated Business Object. For example, CopyDataFrom is the one we'll use to load the Customers ListView on frmParentForm. It assumes that the business object has already been loaded once (in the form's constructor), so it just grabs a copy whenever it needs it. That avoids extra trips to the server (since we can't add customers in this app). CopyDataFrom() requires 2 parameters, which you'll specify in a ListPopulating event (See Listing 3, below). It looks strange at first, but once you've seen it a few times, you'll get used to it.
  3. Which display fields to use; in the process of adding them, they are assigned numbers; these numbers are what you have to refer to in the next step; and
  4. The columns to display; the only oddity is that, instead of column names, you use the field numbers from the left side of the dialog to specify which fields to use in which columns.

A final twist is that, instead of using a table field to fill a ListView column, you can call a function in the RowPopulating event that returns a value to plug into the first column of each row. That's how we'll use that CountNotes() method that we wrote for CustomerNotesBO earlier. The value is assigned in the RowPopulating event of frmParentForm.listView1.

frmParentForm

In frmParentForm, add a ThemedToolStrip and a ListView from the StrataFrame menu, setting the ListView's Dock property to Fill. Next, add a StrataFrame ChildFormDialog. Finally, drop both a CustomerBO and a CustomerNotesBO on the form. These last three items (as well as the ThemedToolStrip) will all appear in the tray at the bottom of the screen (Fig. 13).


The ToolStrip Items Collection property editor (Fig. 14) is straightforward. Add a CommandButton, a separator, and another CommandButton. In the first CommandButton, btnRefresh, set the Text property to "Refresh List"; be sure to change the DisplayStyle to ImageAndText. Set the second CommandButton's Text to "View Customer Notes".


Select the ListView, find the Columns property and add the four columns shown in Fig. 15. (You can also click on the Edit Columns link at the bottom of the property sheet.) You can size the column widths directly on the design surface using your mouse.


Next, open the PopulationDataSourceSettings property editor for the ListView and select CopyDataFrom as the "Method to Execute" (Fig. 16). This will require that we assign values to two parameters in the ListPopulating method, which we'll do below. Set Cust_pk as the Tag value, because the Tag is used to supply the SelectedValue for the ListView. (Refer to Fig. 16 for the next few paragraphs.)

You'll next add four Fields: FirstName, LastName, DayPhone and Email. This is just a field list for the Columns list at the right, so don't try to get creative just yet. Just add the column names and note their sequence numbers.

On the right side of the dialog, you'll define the content for the four columns that we added in Fig. 15, above. The sequence numbers assigned under Fields are surrogates for the column names. It looks a little clumsy, and it is, but it allows you to use format strings to doll up the data you get from the table - like displaying LastName, comma, FirstName in the Name column.

The only exception is the first column's PopulationType is PopulatedThroughEvent; that's where we'll display the number of notes found for the current customer. That's why we wrote that function for the CustomerNotesBO. We'll supply the data below in the RowPopulating event.

The next three columns get their data from the fields at the left side of the dialog. {1}, {0} is (field 2, comma, field 1). Sorry about the zero-based indexing. Blame Bill. Just make sure that the column's data matches the column headings from Fig. 15.


The code for frmParentForm

The code for frmParentForm consists of just four event handlers and a single call in the constructor or Load event to populate the business object:

  1. The constructor (or in VB the Load event), where the CustomersBO1 object is filled and the ListView requeried;
  2. The ListPopulating event, which specifies where to go to copy the data for the list (to avoid multiple trips to the server);
  3. The RowPopulating event to call the function that counts how many notes there are for each customer; and
  4. The code to show the frmNotesList form when the Show child records button is clicked; and
  5. The Refresh button to requery the ListView. This will actually simply copy the

To add an event handler, select an object (a button or the ListView), open the Properties Sheet, click on the little lightning bolt icon at the top of the Properties Sheet, and double-click the event in question. (VB has another way, but if you use VB, you probably already know that.)



The use of casting in the RowPopulating method is there to get IntelliSense to validate the fact that e.BusinessObject is a CustomerBO and has a cust_pk property. I could have instead written it as

e.Values[0].DisplayValue =
  CustomerNotesBO1.GetNoteCount(((CustomersBO)e.BusinessObject).cust_pk).ToString("n0"); }

Requery of the ListView copies customers from the CustomersBO default view, which was loaded in the constructor, so no additional trips to the server are needed. The Edit Customer Notes button is only enabled if customer records exist; and if the user clicks on the Edit Customer Notes button, the result of the childFormDialog1.ShowDialog() call is used to determine whether CustomerNotesBO1 changes are saved or abandoned. Note that you can't configure childFormDialog1's properties until frmNotesList is done. We'll do that in the next step.

Press F5 to verify that the ParentForm ListView populates correctly.

frmNotesList

The frmNotesList form has a ToolStrip and a ListView. The ToolStrip lets users Add, Edit or Delete notes, then either Save or Cancel what they've done before returning to frmParentForm. There's also a ChildFormDialog, but as before, we'll need to finish all three forms before we can set its properties.


The ToolStrip buttons provide the needed functionality. Be sure to select ImageAndText as the DisplayStyle:


Finally, the ListView has just one column - the Subject of each note:


It gets its data by copying the CustomerNotes records previously loaded into CustomerNotesBO1 prior to launching the child form. Select the ListView, open the Properties Sheet, select the PopulationDataSourceSettings property, and click on the three little buttons at the right (Fig. 20 - The important elements are circled.)


The code for frmNotesList

The code for frmNotesList consists of seven event handlers. Take a few minutes to examine each one, especially those that use built-in methods in SF Business Objects. (Note that while the Handles clauses in VB hook up the event handlers, in C# it's done differently, and you have to open the Properties Sheet and explicitly add each handler in order to generate the appropriate hookup code in the form's code-behind.)



frmNotesEditor

frmNotesEditor has just two textboxes, both bound to columns in CustomerNotesBO1 and set inside groupboxes (for titling), and two Buttons:


This is the only form with controls that have a BusinessObject property setting; both of the textboxes have CustomerNotesBO1 as their BusinessObject, and BindingField properties of cn_Subject and cn_Note, respectively.

The code for frmNotesEditor:

The code for frmNotesEditor is shown below: We set the form's DialogResult to either Ok or Cancel based on the button the user clicks on. The ChildFormDialog1 that launched the form detects the form's setting and passes it back to the calling form. In addition, if they cancel, we undo changes to the current row:



Enforcing Business Rules

The CheckRulesOnRow() method call in the Save button event handler sounds pretty impressive. So what rules are we talking about? Business Rules.

"Business Rules" sure makes it sound like we've got the situation under control: We don't have to write code; we just specify some "Business Rules" and we're done. Sounds like a ploy to lull our unsuspecting clients into a false sense of security to me. However, it turns out that 95% of the "business rules" in most applications consist of ensuring that fields aren't left empty. The RequiredFields collection is where you select them. Open CustomerNotesBO by doubleclicking on it in the Solution Explorer, then right-click on the design surface and select Properties. Select the RequiredFields property and click on the three little dots. The resulting Property editor shown below in Fig. 24 lets you check the required fields (Fig. 22).:


I love this feature, because it's a pain in the neck to code each one. Just check the ones you don't want left empty. What could be simpler?

Configuring the ChildFormDialogs on frmParentForm and frmNotesList

Recall that the original objective was to show how to use the StrataFrame ChildFormDialog to simplify this task? We dropped on on both frmParentForm and the frmNotesList form. But we hadn't set their complex properties yet, because all of the required elements weren't present on the forms. They are now. Let's do it.

Open frmParentForm, select the ChildFormDialog1 object in the tray below the form, and press F4 to open the Properties window. Select the BusinessObjectTranslations property and click on Collection (Fig. 23):


Open the frmNotesList form and do the same with its ChildFormDialog1, as shown in Fig. 24:


Press F5 to run the application.

Clean-up

There's just one tiny detail to attend to. By default, when you make changes to business objects, their forms detect the changes and ask your user if they want to save or flush their changes. We don't need that, because this is a snappy little app that assumes the users know what they're doing. So in each of the child forms, set the AutoShowDeleteConfirmation and AutoShowSaveChangesMessage properties to false. Press F5 to run the app again and note the sprightly performance. I feel the need for speed...


Conclusion
top

Parent-child forms are pretty similar, so why reinvent the wheel? This is what a framework is supposed to do; you build your forms, add some table classes, set some settings, add minimal event handling code, and they just work.

See 'ya.

qqq


Copyright(C) Pinter Consulting, 2012Tel: +1 (650) 464-6924
Automated conversion between C# and VB by Visible C#/Visible VB from Tangible Software Solutions