Search  
Friday, September 10, 2010 ..:: Articles » CSLA version 2; what's in it for me? ::..   Login
 CSLA version 2; what is in it for me?
Introduction | Business Objects | Simplify UI | Data Binding | ORM | Business rules | N-level undo | Security | Scalability | Localisation | Best practice | Community | License | Links
 
Show as single page

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.


Business Objects | Page 3 of 14 | Data Binding

      

Copyright 2005 by Primos Computer Services   Terms Of Use  Privacy Statement