Note: This article was created based on Flex 2. Minor changes in the description and code may be necessary before it can be applied to Flex 3.
When developers start building web applications, they tend to make their coding choices to solve a particular problem rather than planning the structure of the application. You might get stuck on a problem, search for a solution, and implement the code. Oftentimes this leads to spaghetti code and hacks that get the job done but do little to ensure the scalability of your application.
In this three-part series, I describe the development of a sample contact manager application.
This series is not a step-by-step building tutorial. In each article, I will give you the source code for each version. I will then step through the code and point out the unique aspects of each version. Through this process, I hope that you will come to recognize and see how the code evolves and moves to the "next level" in each iteration. I use the same application each time so that you can focus more on what has changed in the code and why it's changed. All too often, an article might introduce new concepts but use different application code in each of those articles, leaving you to figure out the conceptual changes. Because you'll be familiar with the app in this series, you'll be able to extrapolate the conceptual changes in each version more clearly.
To complete this tutorial you will need to install the following software and files:
Note: Extract these files into a new Flex project (if you are using Flex Builder) or a generic folder on your system (if you are using the Flex SDK and another editor).
To benefit most from this series, it is best if you:
Let's take a look at the completed application. The contact manager application is comprised of three screens. First is the main screen with the DataGrid that lists all the contacts and has an Add New button. Second is the view contact details screen which is exposed when you click a contact entry in the DataGrid. Third is the enter contact details screen which is exposed when you click the Add New or Edit Details button.
Note: There are three files for the application. However, they do not map one-to-one to the screens mentioned above. Instead, the application is broken into three distinct components that you can reuse later theoretically.
The file main.mxml contains the central component that holds
the other two components. This file also contains the main transition that shows
and hides the detail level of the component. The file contactListComponent.mxml
has the contact list DataGrid and the Add New button. Lastly, the contactDetailsComponent.mxml
file contains the contact details form with its two states (read and write) and
transitions. All three files have mx:Script blocks to house the ActionScript 3.0 code for
variables and functions.
The following sections explain the logic in the application, pointing out the supporting code where appropriate. One thing to note, however, is that I will not cover the transitions—it is beyond the scope of this article. While the transitions are fun to look at, the transition code stays exactly the same in the three versions of the application shown in this series.
First, I declare the main variable to hold the contact list
in main.mxml. I choose to use an ArrayCollection mainly because of its manipulation methods (i.e. addItem) and I prefer Objects over XML:
[Bindable] public var contactListAC:ArrayCollection = new ArrayCollection();
You'll need access to this same variable in the contact list DataGrid. Otherwise, how else would the list know which contacts to show? Therefore, in the file contactListComponent.mxml, notice that the code sets up a matching variable declaration:
[Bindable] public var contactListAC:ArrayCollection;
The code then binds the DataGrid's DataProvider to this variable:
<mx:DataGrid id="mainList"
dataProvider="{contactListAC}" itemClick="itemClicked(event)" width="100%" height="100%">
Note: Unlike the real declaration in
main.mxml, I do not add the new ArrayCollection() because you will pass a reference of the main variable to the
subcomponent. I pass the reference as a parameter in the component
declaration in main.mxml, binding the subcomponent contactListAC to the main contactListAC:
<local:contactListComponent id="contactListComp"
contactListAC="{contactListAC}" width="100%"/>
While I could use a remote object or web
service call to populate the contactListAC variable, I chose not to for brevity and simplicity. Therefore, in the initFunc that's called on creationComplete for main.mxml, I hard coded
the first entry into the system:
public function initFunc():void {
contactListAC.addItem({First:"Tom",Middle:"",Last:"Ortega",Cell:"555-222-3333",Home:"555-333-4444",Address:"1313
Mockingbird Lane", City:"San Jose", State:"CA", Zip:"95134"});
currentState="";
}
When creating this functionality, I know they are going to share a lot of code. In this section, I'll show you how seven functions spread over the three component files accomplish all of these tasks.
To view a contact's details is very
simple. The user merely needs to click an entry in the DataGrid. As shown
above (where I showed that DataGrid's dataprovider being bound to the contactListAC), you see that I have
set up an itemClick event
handler. Whenever a user clicks the DataGrid, I call the following function in
the file contactListComponent.mxml:
public function itemClicked(event:ListEvent):void
{
if (event.rowIndex != 0)
{
this.parentDocument.contactSelectedFunc(
mainList.selectedItem ,true);
}
}
The first if statement ensures that the
item clicked wasn't a header. Once that's verified, I call the contactSelectedFunc in the parentDocument. Using this.parentDocument is just a hack to get at the parent component. Take
a look at that function in the file main.mxml:
public function contactSelectedFunc(
selectedItem:Object,showContactDetails:Boolean):void
{
contactDetailsComp.selectedItem = new Object();
if (showContactDetails)
{
currentState="ShowContact";
if (selectedItem == null)
{
contactDetailsComp.selectedItem = new Object();
contactDetailsComp.currentState = "AddContact";
}
else
{
contactDetailsComp.selectedItem =
selectedItem;
contactDetailsComp.currentState = "";
}
}
else
{
currentState="";
}
}
First, the function sets the selectedItem property in the contact details
component to a new object. Doing so clears any remnants of the last selected
item. Next, the code checks to see if the showContactDetails parameter is true. Since it is, the the currentState is set to ShowContact, which
specifies that the application should show the contact details form. Next, the
code checks whether the selectedItem parameter is null. Since it is not null (I have passed in the selectedItem from the mainList DataGrid), the selectedItem in the contact details component
is set to the passed in selectedItem. Lastly, the contact details' component state is set to nothing. This sets the
details form to the base read-only state.
Once the contact details form is open, the user has the ability to click the Edit Details button. This changes the form from a read-only form to an editable version. In the file contactDetailsComponent.mxml, you can see the button declaration:
<mx:Button label="Edit Details"
click="editSubmit()" id="EditSubmitButton"/>
The button's click event calls the editSubmit function:
public function editSubmit():void
{
if (currentState=="EditContact" ||
currentState=="AddContact")
{
selectedItem.First = firstNameInput.text;
selectedItem.Middle = middleNameInput.text;
selectedItem.Last = lastNameInput.text;
selectedItem.Cell = cellPhoneInput.text;
selectedItem.Home = homePhoneInput.text;
selectedItem.Street = streetAddressInput.text;
selectedItem.City = cityInput.text;
selectedItem.State = stateInput.text;
selectedItem.Zip = zipInput.text;
this.parentDocument.updateContactListFunc(
selectedItem,addNew);
}
else
{
currentState="EditContact";
}
}
First, the code checks the currentState of the contact details
component, which is currently blank. Therefore, the code skips the first block
and runs the else block code, where the currentState is set to EditContact. This
calls a transition and coverts all the labels of the form into text inputs,
allowing the user to edit the details. It also sets the addNew value to false as you are not adding a new
contact. Another thing that the transition sets is the button's label from
"Edit Details" to "Submit Updates." The click event still calls the same
function.
Once the user completes editing and clicks the button, the
application calls the editSubmit function again. Only this time, the currentState is EditContact, so the
application calls the first block in the if/else code. In short, I have created
a selectedItem that houses all
the data from the form, changed and unchanged. I then call the updateContactListFunc in the main.mxml
using the this.parentDocument method again:
public function updateContactListFunc(
selectedItem:Object,newItem:Boolean):void
{
contactDetailsComp.selectedItem = new Object();
if (newItem)
{
contactListAC.addItem(selectedItem);
currentState="";
}
contactListComp.updateTable(selectedItem);
contactDetailsComp.selectedItem =
selectedItem;
contactDetailsComp.currentState = "";
}
In this code snippet, the first lines "blank
out" the selectedItem in the contact details component by setting it to a new
object. Next, the application checks to see if this is a new item, which it is
not. Then, it calls the updateTable function in the contact list component. After that, it resets the selectedItem in the contact details
component to the selectedItem. This is so the contact details form is updated with these new edits instead of the
old unedited information. Lastly, I set the contactDetailsComponent's currentState back to blank, or,
in other words, the base read-only state.
Looking at the updateTable function in the contactListComponent.mxml
file, notice that the function is short and sweet:
public function updateTable(selectedItem:Object):void
{
mainList.selectedItem = selectedItem;
}
This function simply replaces the
DataGrid's currently selectedItem with the item passed into the function as the selectedItem parameter.
Adding a new contact is very similar to editing an existing contact. The biggest difference is the "Add Contact" click in the contactListComponent.mxml file. Here, you see the button declaration:
<mx:Button label="Add New"
click="addNewClick()"/>
The button's click event calls the addNewClick function:
public function addNewClick():void
{
this.parentDocument.contactSelectedFunc(null,true);
}
Note that the addNewClick function calls the exact same contactSelectedFunc function that you called when viewing a contact's details (see Viewing a
contact's details section above for the full function code). The only
difference is that you set the selectedItem as null. This causes the following code block to get executed:
if (selectedItem == null)
{
contactDetailsComp.selectedItem = new Object();
contactDetailsComp.currentState = "AddContact";
}
This code causes the contact detail form
to start in the AddContact state, which is the textinput version of the form, ready for data entry. This state also sets the "Edit
Contact" button's label to "Add Contact." The button still calls the same editSubmit function (see Editing a
contact's details section above for the full function code). Only this time,
the AddContact state has set the AddNew property to true in this
line:
this.parentDocument.updateContactListFunc(selectedItem,addNew);
This condition causes a different bit of
code to execute in the updateContactListFunc of the main.mxml file (see Editing a contact's details section above for the
full function code):
if (newItem)
{
contactListAC.addItem(selectedItem);
currentState="";
}
As you can see, editing and adding a contact are very similar actions. With just a few extra lines triggered by some flags, they can share a lot of the same code.
The "Hide Details" button in the contact details form is very simple:
<mx:Button label="Hide Details"
click="hideDetailsClick()"/>
This click event on this button calls the hideDetailsClick function. As you can
see, that in turn just triggers a state change in the main.mxml file through
the this.parentDocument syntax
again:
public function hideDetailsClick():void
{
this.parentDocument.contactSelectedFunc(null,false);
}
As you can see, the above code accomplishes the job and is an application that works perfectly fine. By many people's measures, it's a success. However, in terms of code reuse and scalability, there are many aspects of the application that need to be reworked for scalability and best coding practices.
The biggest violation against good coding
practices is the use of this.parentDocument. While this is an easy way to get access to the parent component, it comes at the cost of reusability and flexibility. What if you wanted to use that list in a different
application? If you wanted to, the component that housed it would need to
declare those functions referenced in this.parentDocument and use the exact
same syntax. What if you wanted to nest the component differently? For
example, right now the contactListCompoment is nested in the main component. What if you added a new contactApplication component between
main and contactListComponent? Your this.parentDocument would
fail and you'd have to rework/rewrite all instances of it.
Moreover, the code in this tutorial uses array
collection referencing. I want only one copy of contactListAC in our main component, with all other subcomponents using only a reference. However, there must be a better way to do than demonstrated here. Passing a
reference as shown in this tutorial is no better than using this.parentDocument. In the same
example above, I'd have to create and pass in a reference of contactListAC in the contactApplication component for the
sole purpose of passing it to the contactListComponent.
If these methodologies are no good, then why show them? I did so to expose them as the hacks they truly are. If you find yourself coding like this and find yourself working too hard to go back and make changes, then this series is for you. If you have wondered how to achieve certain logic that these hacks have shown, do not take to the hacks lightly.
Check out Part 2: Adding architecture to your code design, which covers concepts such as custom events and value objects to code architected applications for scalability, code reuse, and project maintainability. Next, check out Part 3: Revising code within the Cairngorm framework, where I show how you can achieve this logic with much cleaner code.
Thomas Ortega II is an active member of the Flex community and works for Workday, Inc. He runs the Silicon Valley Flex User Group where he gives free Flex training. He also is a coordinator of the 360|Flex conference. You can find Tom on his blog.