Collecting Customer Info - OrderProfile.aspx

Once the customer has picked his items it's time to place the order. The ordering process is broken down into two separate forms - the first for handling the customer information and the second for handling the payment portion of the invoice.

The first form is the OrderProfile page which is similar to the Profile page. Both of these pages deal with retrieving customer information which is then stored in the wws_Customers table that makes up the customer's profile. The form looks like this:

The form collects all the customer information including billing and shipping address. The Shipping Address can be expanded, but is only shown if the user selected to actually have physical shipment made in the Shopping Cart's options. Otherwise this option is not available. When clicked the Shipping Address section pops open.

This form also is the first form that really uses DataBinding to bind data directly from the database fields to the ASP.Net form using the wwWebDataControls. Check out the following view in VS.Net:

You can see the same form in design mode. Notice the Shipping Section which is now expanded (the opening and closing is handled via JavaScript code more on this later). All the fields of this form are databound using the wwWebDataControl subclasses. Notice in the image that the BindingSource is set to Customer.DataRow and that the BindingSourceMember is set to Company. This tells the control to bind and unbind against the Customer DataRow member and the Company field. In this case we're binding to a table row and a field but you can also bind to object properties in the same way.

The actual binding is then handled in the form by calling the BindData() method when the form is first loaded and the UnbindData() method when the Save button is clicked. Here's the Page_Load code:

private void Page_Load(object sender, System.EventArgs e)
{
	// *** Make sure user went to the ShoppingCart page first
	if (Session["InvPk"] == null) 
	{
		MessageDisplay.DisplayMessage("Your Shopping Cart is empty",
			"Your shopping cart either has no items in it or your browser does not support or has Cookies disabled.<p>" +
			"Please check your browser settings to make sure that your browser supports Cookies");
		return;
 	}

	// *** Must get the Shipping Address info
	this.ShipInfo = (ShippingInfo) Session["ShippingInfo"];

	// *** Create a customer object	
	this.Customer = WebStoreFactory.GetbusCustomer();
			    
	// ***  Retrieve the UserId from the Cookie/Session settings
	this.UserId = WebStoreCookie.GetUserId();

	if (!this.IsPostBack) 
	{
		// *** Load and display data in form
		this.LoadCustomer();
		this.BindData();
	}

}


public bool LoadCustomer() 
{
	if (!this.Customer.LoadByUserId(this.UserId)) 
	{
		if (!this.Customer.New())
			return false;

		this.lblConnectToProfile.NavigateUrl="ProfileLogon.aspx?page=OrderProfile.aspx";
		this.lblConnectToProfile.Text="Logon to your Profile";
	}
	else 
	{
		this.lblConnectToProfile.NavigateUrl="ProfileLogon.aspx?Action=Clear&page=OrderProfile.aspx";
		this.lblConnectToProfile.Text="Unlink your Profile";
	}
	
	this.CustomerRow = this.Customer.GetTypedDataRow(false);

	int CustPk = (int) this.CustomerRow.Pk;
	Session["CustPk"] = CustPk;

	if (!this.Customer.ShippingAddress.LoadByCustPk(CustPk)) 
	{
			this.Customer.ShippingAddress.New();
			
			// *** Assign Shipping Address defaults
			if (this.ShipInfo.ShipToCustomer) 
			{
				DataRowContainers.wws_shippingaddressesRow ShipRow = this.Customer.ShippingAddress.GetTypedDataRow();
				ShipRow.Name  = this.CustomerRow.Firstname + " " + this.CustomerRow.Lastname;
				ShipRow.Company = this.CustomerRow.Company;
				ShipRow.State = this.CustomerRow.State;
				ShipRow.Countryid = this.CustomerRow.Countryid;
			}
	}

	return true;
}

Notice that there is very minimal code here - certainly nothing that deals much with databinding. This code deals with setting up the form for display by checking to make sure that we have items to process (InvPk in Session) and if so creating a business object. The code then tries to load a remembered customer if we have a persistent Cookie for this customer. If the user is a first time customer and did not previously log in a new customer object is created. LoadCustomer is responsible for creating a Customer object so it creates a new one if the customer is new or tries to retrieve the customer's existing profile.

The user can optionally try to reattach to his profile by using the ProfileLogon which redirects to the ProfileLogin.aspx page. If the user logs in successfully he then is redirected back to the OrderProfile page with all of the Profile information filled in. Remember Profile and Customer information are really the same things - just different terminology for the application.

At this point the user sees either a blank form or his previous customer information which can now be edited. As I mentioned the actual databinding occurs the first time the page is displayed (!IsPostBack) and is as simply as calling the BindData() method of the form. Besides the property values on the controls there's no further need to do anything for databinding to occur. For more detail on how Databinding works see the Databinding Web Control Classes.

Saving Data

Once the user's made changes to his customer profile he can click the Save button. What happens next is that the data unbinding occurs by calling this.UnbindData(), followed by validation and finally saving the data back through the business object. The code for this looks like this:

private void btnProfileSubmit_Click(object sender, System.EventArgs e)
{
	// *** Reload the data from disk to be updated
	this.LoadCustomer();

	// *** Bind back the control data to the underlying datasource
	// *** Binding back to this.Customer and this.Customer.ShippingAddress!
	this.UnbindData();

	// *** Make sure the UserId is current
	this.CustomerRow.Userid = this.UserId;


	// *** Check for binding errors and bus object validation errors
	if (!this.Customer.Validate() || this.bindingErrors != null)  
	{
		this.AddValidationErrorsToBindingErrors(this.Customer.ValidationErrors);
		this.ShowError(this.bindingErrors.ToHtml());
		return;
	}
		
	// *** Must also update Shipping info
	if (this.chkShippingAddress.Checked) 
	{
		if (!this.Customer.ShippingAddress.Validate())  
		{
			this.AddValidationErrorsToBindingErrors(this.Customer.ValidationErrors);
			this.ShowError(this.bindingErrors.ToHtml());
			return;
		}

		DataRowContainers.wws_shippingaddressesRow ShipRow = this.Customer.ShippingAddress.GetTypedDataRow();

		ShipRow.Custpk = this.Customer.Pk;
		if (!this.Customer.ShippingAddress.Save()) 
		{
			this.ShowError(this.Customer.ShippingAddress.ErrorMessage);
			return;
		}

		Session["ShippingAddressPk"] = ShipRow.Pk;

		// *** Must update the ShipInfo structure with our shipping info
		this.ShipInfo.State = ShipRow.State;
		this.ShipInfo.CountryId = ShipRow.Countryid;
	}

	if (!this.Customer.Save())
		this.ShowError(this.Customer.ErrorMessage);
	else 
	{
		Session["ShippingInfo"] = this.ShipInfo;
		Server.Transfer("OrderForm.aspx");
	}
}

The key here is that we need to reload the customer object so we have the correct base data we're binding to. Why? Well, the form does not contain all the customer data, such as the last time the customer ordered or when the record was created. These values are still required in our object so we simply unbind the data which will update the fields that we're bound to. Yes, it's that simple!

We do have to check for errors though. Unbind() creates a BindingErrors collection on the form (or optionally returns it). You can check to see if this collection is null and if it is there are no errors. If not you can call the ToHtml() method of it to create an error message that you can display on the form. The handler will also automatically place icons next to the controls that have errors in them (configurable via the control properties):

The first error is simply generated by the IsRequired property on the control, which automatically checks to see if the field is left empty and sets an error. But you can also fire errors from the business object as is the case with the second error for the State field. For example, here is what the Customer object's Validate() method looks like:

public override bool Validate()
{
	this.ValidationErrors.Clear();

	if ( wwUtils.Empty(DataRow["Zip"]) )
		this.ValidationErrors.Add("Postal Code can't be empty.","txtZip");

	string State = (string)DataRow["State"];

	if (wwUtils.Empty(State)  && 
		((string) DataRow["CountryId"] == "US" || (string) DataRow["CountryId"] == "CA") )
		this.ValidationErrors.Add("State must be provided.","txtState");

	if (this.ValidationErrors.Count > 0)
		return false;

	return true;
}

The business object has a ValidationErrors collection that you can add any errors to. Optionally you can assign a control name that is assigned the error. This is optional and highly coupled to the implementation, however, it's also fairly common to use consistent property names for the user interface binding.

The form can then merge the ValidationErrors of the business object into the BindingErrors of the form for a unified error display mechanism with this line of code

this.AddValidationErrorsToBindingErrors(this.Customer.ValidationErrors);

Here the validation errors are added and if a control name can be matched to a specific control on the form that is databound then the error icon is displayed next to it as well.

This process makes databinding very painless as you can easily assign property values and then calling the BindData and UnbindData methods to handle the databinding. All that's left is validation and display of the data.

Handling the Shipping Address

Alot of the code on this page deals with the optional Shipping Address. The shipping address is implemented as a separate and free standing business object that is not directly associated with a customer. The reason for this is that each invoice actually stores its own Shipping information with the order, because the Shipping Information should not change once an invoice has been entered so there's a record where the order was sent to.

When the customer is saved the shipping address is also saved so that it can be reused in the future. But mainly the Shipping Address is finally retrieved and used by the Invoice to copy the data into its own tables.

There's actually a couple of JavaScript functions in the code that handle displaying the Shipping Address droppable checkbox:

<script language="javascript">
function ShippingAddress(SetVisible)
{
   for (x=1; x < 8; x++) 
   {
         CName = "Shipping" + x.toString();
         Control = document.getElementById(CName);
         if (Control != null) {
            if (SetVisible)
               Control.style.display="";
            else
               Control.style.display="none";
         }
   }
   
   // *** Show or hide the display Separator
   Control = document.getElementById("Shipping0");
   if (SetVisible) 
   {
      Control.style.display="none"; 
      
      if (document.getElementById("txtShipCompany").value == "" && 
            document.getElementById("txtShipName").value == "")
         {
            document.getElementById("txtShipCompany").value = document.getElementById("txtCompany").value;
            document.getElementById("txtShipName").value = document.getElementById("txtFirstname").value.replace("  ","") + " " + document.getElementById("txtLastname").value;
            
            SelectListItem( document.getElementById("txtShipState"),document.getElementById("txtState").value);
            SelectListItem( document.getElementById("txtShipCountryId"),document.getElementById("txtCountryId").value);
         }
   }
   else 
      Control.style.display="";  
}

function SelectListItem(loList,lcValue) 
{
   loList.selectedIndex = 0;

	for (x=0;x<loList.length-1;x++) {
   if (loList.options[x].value == lcValue) {
	      loList.selectedIndex = x;
	      break;
	   }
	}
}
</script>

This messy code deals with popping opening the shipping address dialog and enabling all of the items. This is so that theses controls don't post back to the server when they're not active, otherwise we'd get errors that the controls are empty or otherwise contain invalid values. When not visible no binding errors occur.

How this section displays requires checking whether physical shipment occurs and whether the user has already entered some data into it (after posting back with an error perhaps, or after returning to this form:

	// *** Handle properly displaying the Shipping Address
	// *** If we ShipToCustomer show the section
	// *** If we already selected to ship show the section opened
	this.RegisterStartupScript("ShowShipping",
@"<script>
document.getElementById('trShippingHeader').style.display='" + 
(this.ShipInfo.ShipToCustomer ? "" : "none") + @"';
ShippingAddress(" + this.ShipInfo.UseShippingAddress.ToString().ToLower() + @");
</script>");

All the settings are stored in the ShipInfo structure. As mentioned earlier this object handles all settings related to the shipping process. On this form the UseShippingAddress field is bound to the shipping option checkbox for example, so changing the checkbox value directly affects the ShipInfo object.

Note that this demonstrates real well how the databinding works. We're actually binding against several separate business objects and their data on this form: Customer business object DataRow, ShippingAddress DataRow and the ShipInfo object which is property binding.


It's ironic that the most complicated part of this form is the shipping address handling, which is in theory a simple process. Ok, it's time to finish off our order by collecting order information and placing our order.


© West Wind Technologies, 1996-2018 • Updated: 12/15/03
Comment or report problem with topic