Deliverable: Minimise, and simplify, the code in the UI
The part of our application which is most subject to change is
usually our UI. For this reason it is highly desirable to simplify
and minimise UI program code, as this will ease, deskill, and reduce
the risk involved in future modifications. We can help achieve this
objective by creating high function Business Objects for our UI to
use. This will reduce the complexity, clutter, and general plumbing
code that would otherwise be required inside the UI tier.
The fact that our UI tier is just dealing with high level BO's
will also insulate it from change, should we decide to migrate up to
some future Microsoft technology such as the shift from .Net Remoting
to Indigo, or a change from ADO .Net to whatever may come next.
I
will demonstrate the simplicity that we can achieve in our UI tier
via a small application and a few BO's which I have written using SQL
Server's Northwind database.
Firstly let us start small by having a dialogue box that displays
the list of those countries for which there are customers. We select
a country, and the form changes to a list of customers within that
country. When we select a customer the dialogue box auto-closes,
returning the ID of the chosen customer to the calling program.
There is a facility to go back and chose a different country
before selecting our customer.
Here
is the VS2005 designer for the above dialogue box. On the left we can
see that my application has a number of BO's available to it. At the
bottom of the form we can see that I have dragged two of these data
sources onto my form. This has created a BindingSource component for
the CountryList BO, and the another for the CustomerList BO.
The designer doesn't show the listbox which will contain the data
as I have created a generic User Control for these, and they will be
added into the form a run time.
The constructor for the User Control is as follows. The user
control contains a listbox. We can see that the listbox's data
binding properties are being set so that it knows which datasource
field to display in the list, and which field to use as the displayed
row's ID. At this stage the listbox hasn't been informed of the
identity of the datasource.
public CountryCustomerSelectionList(
string displayMember,
string valueMember,
Color backColour)
{
InitializeComponent();
this.BackColor = this.BackColor;
this.Dock = DockStyle.Fill;
this.listBox1.DisplayMember = displayMember;
this.listBox1.ValueMember = valueMember;
}
Now back to the dialogue form whose designer window we saw above.
Here is it's Load event. We can see that we have added two of the
user controls to our form, one for each of the BindingSource
components that we dragged onto the form. If you refer back to the
designer window you can see that the property names Name, Id, Value
and Key are coming from our BO's.
private void CountrySelect_Load(object sender, EventArgs e)
{
_companyList = new CountryCustomerSelectionList(
"Name",
"Id",
this.BackColor);
groupBox1.Controls.Add(_companyList);
_countryList = new CountryCustomerSelectionList(
"Value",
"Key",
this.BackColor);
groupBox1.Controls.Add(_countryList);
_listBeingDisplayed = _countryList;
SetUIForViewMode();
}
Here
is the class diagram for the CustomerList BO. We can see that it has
a GetList method which is marked public and static. Since the method
is static it can be used without needing to firstly create a
CountryList instance. We can also see that it returns a CountryList
object. This is how we get our list of countries. We simply call
CountryList's GetList method, and we get passed back populated list
of countries. We see this in the following method.
At the close of the above event handler we set the
_listBeingDisplayed variable to initially refer to the Country list,
and we then called the SetUiForViewMode method. Here is that method.
We now get to use our BO's. Initially we will flow into the 1st
else clause. We can see that the country list is acquired and passed
into the BindingSource's DataSource property.
private void SetUIForViewMode()
{
if (_listBeingDisplayed == _countryList)
{
if (_countryList.ListAlreadyLoaded)
_countryList.BringToFront(); // use cached copy.
else
{
countryListBindingSource.DataSource = CountryList.GetList();
_countryList.LoadList(countryListBindingSource);
}
selectButton.DialogResult = DialogResult.None;
resetCountryButton.Enabled = false;
this.Text = "Select a country";
}
else
{ // is "customer" mode
customerListBindingSource.DataSource = CustomerList.GetList(_countryId);
_companyList.LoadList(customerListBindingSource);
selectButton.DialogResult = DialogResult.OK;
resetCountryButton.Enabled = true;
this.Text = String.Format("Select a customer from {0}.", _countryId);
}
}
The second else clause in the above method shows us acquiring the
customer list for the selected country. We call the CustomerList's
static GetList method passing the selected country's ID. We are
returned the populated list of customer for that country.
The listboxes were populated by the user control's LoadList
method. Essentially it just assigned the Binding source to the
listbox's DataSource property. The use of our BO's has made the tasks
of getting the list of countries and customer very easy for the UI.
public void LoadList(BindingSource dataBindingSource)
{
this.listBox1.SuspendLayout();
this.listBox1.DataSource = null;
this.listBox1.DataSource = dataBindingSource;
this.listBox1.ResumeLayout();
_listAlreadyLoaded = true;
this.BringToFront();
}
The dialogue box did have a couple of buttons. Here are their click
handlers to complete the picture of what is happening.
/// <summary>
/// The user has selected something.
/// The first time through they have selected a country. We toggle the list
/// from a list of countries, into a list of that country's customers. When
/// the user has also selected a customer the form will auto-close as the
/// "select" button will have been set-up to return a modal result.
/// </summary>
private void selectButton_Click(object sender, EventArgs e)
{
// store selection; is either a country or a customer
if (_listBeingDisplayed == _countryList)
{
_countryId = _listBeingDisplayed.listBox1.SelectedValue.ToString();
_listBeingDisplayed = _companyList;
SetUIForViewMode();
}
else
_customerId = _listBeingDisplayed.listBox1.SelectedValue.ToString();
}
/// <summary>
/// Offer the user the option of changing their country selection.
/// </summary>
private void resetCountryButton_Click(object sender, EventArgs e)
{
_listBeingDisplayed = _countryList;
SetUIForViewMode();
}
We have seen quite a bit of code, but almost all of it is just
setting up and controlling the UI. This is good. The use of BO's has
extracted away all of the logic required to obtain the data from our
data store. All we saw was:
use of data binding to connect our form control to the BO:
..... this.listBox1.DataSource =
dataBindingSource;
tell the form control which properties to display: .....
this.listBox1.DisplayMember =
displayMember; etc
ask the BO for the data: ..... CountryList.GetList();
We should also take note of what we didn't see.:
no coupling between our UI and our current data store (we use
SQL SERVER currently, but who knows if this will always be the
case?)
no SQL, ADO, nor any logic to handle cross tier transport
no mapping showing just which table rows get used for what
purposes
We have been allowed to just focus upon UI issues. This means that
the UI programs will be clearer, and we have minimised the risk
should we need to open up the UI again to make changes to it.
We
would have expected that simple functionality would only require
negligible UI program code. The good news continues as we extend our
application to provide more complex functionality. Here we have a
form allowing to update our selected customer and also their order
headers. We have done some data entry, but have broken some of the
business rules. We have blanked out the Contact name field and we can
see that this is not allowed. We have also erroneously set a freight
cell to something that is neither a zero nor a positive number. The
error indication fields have explanatory tooltips.
We can also see that the save button has been disabled as the BO
is now in an invalid state. The save button normally operates to save
any customer level, and/or order level, changes to the database. We
can also see a cancel button which would undo all data entry changes,
which haven't already been committed via an earlier successful press
of the save button. This undo function is achieved without the need
to hit the database again to refresh the data.
We
get a lot of assistance from VS2005 and our BO's when designing such
a form. Here is what VS2005's drag and drop support has automatically
created for me when I dragged the Customer BO, and then it's embedded
Orders BO, onto a blank Winform. As you can see it is more a matter
of tailoring defaults, and deleting what you don't want, than
building the UI from scratch.
The following code handles the calling of our dialogue box to
select the customer to be further processed. It then uses the
Customer BO to obtain the customer by ID. As we saw in the above Data
Sources window, the customer BO contains an embedded Orders BO which
contains all of the customer's orders. All of this is delivered to us
by the statement “Customer.GetCustomer(selectForm.CustomerId);”
We then connect the customer and it's embedded Orders BO to the
BindIngSource components created by the drag and drop mentioned
above, This will populate all of the customer level fields, and the
grid of orders.
using (SelectCountryForm selectForm = new SelectCountryForm())
{
if (selectForm.ShowDialog() == DialogResult.OK)
{
try
{
_customer = Customer.GetCustomer(selectForm.CustomerId);
if (_customer != null)
{
_customer.BeginEdit();
customerBindingSource.DataSource = _customer;
customerOrdersBindingSource.DataSource = _customer.Orders;
if (!CustomerGroupBox.Visible)
CustomerGroupBox.Visible = true;
SaveButton.Enabled = _customer.IsValid;
_formTitlePart2 = string.Format("processing a customer from {0}.", selectForm.CountryId);
this.Text = this.FormTitle;
}
}
catch (Csla.DataPortalException ex)
{
MessageBox.Show(ex.BusinessException.ToString(),
"Data load error", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(),
"Data load error", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
}
}
}
Here is the code which ensures that the “save” button
becomes disabled and enabled as the user makes, and then corrects,
any data entry changes which violate our business rules.
private void customerBindingSource_CurrentItemChanged(object sender, EventArgs e)
{
SaveButton.Enabled = _customer.IsValid;
}
private void customerOrdersBindingSource_CurrentItemChanged(object sender, EventArgs e)
{
SaveButton.Enabled = _customer.IsValid;
}
Here is the code which handles our “undo” feature.
private void cancelButton_Click(object sender, EventArgs e)
{
_customer.CancelEdit();
_customer.BeginEdit();
}
And here is the code handling data updates to the database. A
successful update will return us a new object. This is to allow for
any triggered updates which may be generated from within the database
server, or by our business rules. It would also required if we had
implemented a “first write wins” concurrency scheme, as
we would need a new set of timestamps from the database server.
The recommended technique is to attempt save a cloned version of
our BO. Provided the save is successful, we then rebind to the new
object which is returned by the save operation. Should the save fail
for any reason, the UI is still attached to the original BO which
cannot have been partially corrupted by the aborted save operation.
This topic is outside the scope of this article, but as general
indication remember that our save of the customer BO is potentially
updating multiple rows in the Orders SQL table as well as a row in
the Customer table.
private void SaveButton_Click(object sender, EventArgs e)
{
customerBindingSource.RaiseListChangedEvents = false;
customerOrdersBindingSource.RaiseListChangedEvents = false;
/* Close off undo support for any changes to our BO. Note that
* we need to do this for the Orders collection as well as at
* the Customer level. Although the Customer level call will
* have handled the imbedded collection, .Net data binding will
* have started an implcit undo session for the cuurent row.
* We need to close off this implicit edit as well as the one
* that we explicitly caused by our BeginEdit. */
customerBindingSource.EndEdit();
customerOrdersBindingSource.EndEdit();
Customer temp = _customer.Clone();
try
{
_customer = temp.Save();
customerBindingSource.DataSource = null;
customerOrdersBindingSource.DataSource = null;
customerBindingSource.DataSource = _customer;
customerOrdersBindingSource.DataSource = _customer.Orders;
}
catch (Csla.DataPortalException ex)
{
MessageBox.Show(ex.BusinessException.ToString(),
"Error saving", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "Error saving",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
finally
{
customerBindingSource.RaiseListChangedEvents = true;
customerOrdersBindingSource.RaiseListChangedEvents = true;
}
}
Again our UI is free from any SQL or database related code.
We also need to notice that it didn't contain any of our business
rules either. These have been encapsulated inside our BO's
The
error indicator that came on and off, as data entry changes broke and
re-fixed the business rules, virtually come for free. We drag an
Error component on the form, and connect it's datasource property to
the BindingSource component created by the VS2005 drag and drop
mentioned above. That is all we need to do. Since the CSLA base
classes implement the IDataErrorInfo interface, the rest is handled
for us, including the display of the tooltip messages coming from our
BO's business logic.
The UI may also include some logic to handle authorisation.
Depending upon the user's authorised roles, some properties, or whole
BO's, may need to hidden or made read-only.. This task is also
greatly assisted by the CSLA framework and will be shown in a
separate section of this article, but hopefully I have already made
my point, which was that the adoption of the framework has given
strong assistance towards the goal of simplifying the UI sections of
our application.
I have only shown a WinForms example here. Rocky's book, and
downloadable sample application, extend this into a Web Forms
application and Web Services interface. His example is also much more
sophisticated that the simple example I have shown here.
|