Detail Record manipulation (one to many)

Some business objects work primarily on a single record. Customer, Invoice, Inventory Item are good examples of typical single record bus objects. Other business objects tend to be treated as lists or children of another object. LineItems is an example of a list based based business object which is accessed in both single record and list modes.

To facilitate the process of creating lists that are referenced wwBusiness creates list queries in a special table called <tablename>_List and includes methods to load and save this table automatically with the wwBusiness.LoadDetailItems() method, and access the table and row with shorthand syntax.

For example, the lineitems of the store are loaded with a custom method that looks like this:

public virtual int LoadLineItems(int InvPk) 
{
	return this.LoadDetailItems(InvPk,"InvPk");
}

This method creates a table wws_lineitems_List. To access this table you could now use syntax like this:

DataTable dtLineItems = Invoice.LineItems.GetDetailTable();
string Sku = (string) dtLineItems.Rows[0]["Sku"];

To retrieve a row directly by index you can use:

DataRow drItem = Invoice.LineItems.GetDetailRow(0);
string Sku = (string) drItem["Sku"];

Both of these methods are only helper methods that you can use to get a shorthand reference to the actual data in the details. At this point you can make changes to the items in the datarows:

drItem["Descript"] = "Updated description of this sku.";

Changes are buffered at this time in the DataSet. To save the detail table you can then call the generic SaveTable() method and pass the list table. For consistency though it's usually a good idea to implement an explicit method to save the detail. The implementation of the corresponding SaveLineItems method then uses the SaveTable() method of the business object to write the detail table back to the database:

public virtual bool SaveLineItems(bool UpdateInventory) 
{
	if (!this.IsTemporaryLineItem && UpdateInventory) 
	{
		// *** Update the inventory for all items
		if (!this.UpdateInventoryFromLineItems())
			return false;
	}

	// *** Write the changes out to the database
	return this.SaveTable(this.Tablename+"_LIST",this.Tablename);
}

One to many Relations

With this business object implementation it's now easy to attach this business object to a parent object such as an invoice. The invoice simply gets a reference to the LineItems object.

public busLineItems LineItems = null;

Then the Invoice.Load() method of the busInvoice object is overridden to handle loading the lineitems (and in this case also a customer record):

public override bool Load(int Pk) 
{
	if (!this.LoadLineItems(Pk))
		return false;

	if (!base.Load(Pk))
		return false;

	this.LoadCustomer((int) this.DataRow["CustPk"]);

	return true;
}

Note that you also need to override any other methods that load Invoices like busInvoice.New():

public bool New( int CustPk) 
{
	// *** Get a new Invoice first
	if (!base.New())
		return false;
	
	// *** Try to load any lineitems if any (temporary or real)
	if (!this.LoadLineItems(0) ) 
		return false;

	if (!this.LoadCustomer(CustPk))
		return false;
	
	return true;
}

Here LoadLineItems(0) is used to tell the detail method to retrieve an empty table that has the full table structure. 0 is a special value for a PK and usually returns blank items.

After these implementations you can then use:

busInvoice Invoice = new busInvoice();

Invoice.Load(1121);  // Load an invoice by pk

DataRow drItem = Invoice.LineItems.GetDetailRow(0);
string Sku = (string) drItem["Sku"];

To access item 2 you'd simply call GetDetailRow(1) and so on. Remember the invoice is entirely in memory at this point until you save.

To add a lineItem there are several overloads on the LineItem object, but the easiest is this one:

public virtual bool AddItem(busLineItem LineItem) 
{
	LineItem.CalculateItemTotal();

	// *** Add to LineItem Row to internal DataSet
	DataTable dtLineItems = this.GetDetailTable();
	if (dtLineItems == null)
		this.LoadLineItems((int) LineItem.DataRow["InvPk"]); // must create the DataTable
	else
		dtLineItems.ImportRow(LineItem.DataRow);

	return true;
}

To call this you'd use:

busLineItem LineItem = new busLineItem();
LineItem.New();

LineItem.DataRow["Sku"] = "WWHELP";
LineItem.DataRow["Descript"] = "West Wind Help Builder";
... // set more properties

Invoice.LineItems.AddItem(LineItem);

Other overloads also provide the ability to directly add lineitems based on a Sku, which retrieves data from the Inventory table etc. With those you simply pass a parent invoice pk, a SKU and a qty and it does the rest.

Saving Data

Up until this point we've manipulated the data, but we haven't saved it. All the data is cached locally in the DataSets of the business objects. To save we need to do several things: Implement a save method on the line items then have the invoice's Save() method call that method when it saves.

Here's the LineItems.SaveLineItems method:

public virtual bool SaveLineItems(bool UpdateInventory) 
{
	if (!this.IsTemporaryLineItem && UpdateInventory) 
	{
		// *** Update the inventory for all items
		if (!this.UpdateInventoryFromLineItems())
			return false;
	}

	// *** Write the changes out to the database
	return this.SaveTable(this.Tablename+"_LIST",this.Tablename);
}

Note that wwBusiness.SaveTable() takes care of updating the data correctly removing and adding records as needed to the underlying database if you're editing items.

This method is then called from Invoice.Save():

public bool Save(bool SaveFromTemporaryLineItems) 
{
	if (this.AutoValidate && !this.Validate()) 
		return false;

	// *** Start by saving the customer
	if (this.Customer != null) 
	{
		if (!this.Customer.Save()) 
		{
			this.SetError(this.Customer.ErrorMessage);
			return false;
		}

		this.DataRow["CustPk"] = this.Customer.DataRow["Pk"];
	}

	// *** Recalculate the invoice
	this.InvoiceTotal();

	int InvPk = (int) DataRow["Pk"];
	
	// *** Now let's save the lineitems
	this.LineItems.SaveLineItems(true);

	// *** Now let's save ourselves - the invoice header
	if (!base.Save())
		return false;

	return true;
}

As you can see there's some business logic in this code and I used a non-default overload of the Save() method to show here. The parameterless version simply calls this method.

The key here is that this code is easy to read and write because it all deals with high level methods. Writing code like this is actually quite logical because you can incrementally build the logic.

Summary

And voila you've walked through the whole process of using a business object that implements several relationships entirely through the business object access layer.

Now you might say that this is a lot of work - but remember that we did more than handle relations in this code - we also addressed a number of business logic issues here - issues that you would still have to address if you were using some other relation mechanism.


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