Sunday, October 31, 2010

Multi-Tier Applications

Reading the MSDN forums, I can’t count the number of times I see posters putting all their eggs in one basket, so to speak. Their UI forms contain their Business logic AND their Data Access logic. Their DataSets are defined in the same UI project. Microsoft and Visual Studio, unfortunately, makes this too easy to do … what with all the drag-and-drop stuff from the Server Explorer directly onto a Form designer. I’m sorry, but this is really a great big no-no!!!

These activities should be broken up into multiple tiers, or layers. To simplify, think of three different projects/dlls in your solution. MyUI, MyBiz, and MyDataAccess (I'd actually throw in a 4th one, MyWebService, but let's not complicate matters at this point. Oh wait, I'd have a separate DataSet project too, but as I said, let's keep it simple for now). In your MyUI project, you'd have all the different forms that you plan to use in your UI. The same for MyBiz and MyDataAccess projects, all the different Biz classes and DataAccess classes.

So, to start, your form would be similar to this (to get your data when the form first opens):

using MyCompany.MyApp.Business.MyBiz;

namespace MyCompany.MyApp.WinUI.MyUI
{
public class MyForm : MyBaseForm
{
private long CustomerKey;
private MyDataSet dsData;
private CustomerBiz oBiz;

public MyForm(long key)
{
this.CustomerKey = key;
InitializeComponent();
this.FillData();
}

public void FillData()
{
// To simplify, I'm directly calling a Biz class.
// In reality, I use a Web Service here instead
// which in turn calls the Biz class.

oBiz = new CustomerBiz();
dsData = oBiz.GetCustomer(this.CustomerKey);
}
}
}

Now in your MyBiz project, you'd have a Biz class:

using MyCompany.MyApp.DataAccess.MyDataAccess

namespace MyCompany.MyApp.Business.MyBiz
{
public class CustomerBiz
{
private MyDataSet dsData;

public MyDataSet GetCustomer(long CustomerKey)
{
CustomerAccess oDA = new CustomerAccess();
this.dsData = oDA.GetCustomer(CustomerKey);

// if you have other Biz things to do to this customer
// do it here before returning the DataSet

return this.dsData;
}
}
}

And, lastly, in your MyDataAccess project, you'd have this class:

namespace MyCompany.MyApp.DataAccess.MyDataAccess
{
public class CustomerAccess
{
public MyDataSet GetCustomer(long CustomerKey)
{
// Here's where you'd put all the SqlCommand and DataAdapter stuff
// and fill your DataSet.

return dsData;
}
}
}

Now, that's the "simple" version, just to get the concept. Let's take it a step further:

We have 4 "layers" ... UI, Web Services, Business, Data Access.

These are more than 4 projects, because each layer is further broken down by module. Let's take the DataAccess layer as an example:

As in all our layers, there are DataAccess parent classes from which all DataAccess classes inherit. These parent classes have all the basic functionality needed for DataAccess and we consider it part of our "framework" ... it has it's own project. See my 3-part DataAccess series for more info about this:

http://geek-goddess-bonnie.blogspot.com/2009/09/dataaccess-part-i.html
http://geek-goddess-bonnie.blogspot.com/2009/10/dataaccess-part-ii.html
http://geek-goddess-bonnie.blogspot.com/2009/10/dataaccess-part-iii.html

Each module in our app has a separate DataAccess project. So, we'll have a DataAccess.Personnel project and a DataAccess.Inspection project, etc. and the classes in those projects inherit from the parent classes in the "framework" project. (As you probably know, these separate projects become separate .DLLs).

The Business layer got a little more complicated, but the architecture of it is the same. We actually have 2 Business layers ... server-side and client-side. The server-side classes remain on the server where they are accessed from the Web Services. The client-side classes are brought down to the client from the server to be used by the UI classes, but they can also be used on the server.

So, that's it in a nutshell. I think it's a good start, to get you thinking about how you should structure your application.

10 comments:

  1. Hey, this is really nice I like it.
    I've been programming in layer for a while now, but I never got in a project where I might need WebServices to be my 'bridge' between business classes and GUI, how would you implement that in C# if you had a webservice that should return a business class such a Validator class or any other thing you might think of?

    Great blog!
    regards
    JP

    ReplyDelete
  2. Hi JP,

    My past experience with Web Services has been the old-style asmx Web Services. And I always advocated returning simple types, such as strings, so in the past my Web Service methods never returned classes. I only returned XML that contained the data from a DataSet ... to do that, you return MyDataSet.GetXml(), which just returns the data in XML format (not schema, only data). On the client side then, you'd take the XML string and plug it into your DataSet like so:

    StringReader sr = new StringReader(xml);
    MyDataSet.ReadXml(sr, XmlReadMode.InferSchema);
    MyDataSet.AcceptChanges();

    Now, going forward, I'd recommend WCF instead of the old-style asmx Web Services. WCF supports more complicated types because you use Contracts and such (Contracts are the Interfaces that must be used with a particular Service). I'm not even close to being a WCF expert, so I can't say a whole lot more about it, other than to recommend its use.

    Not much help, I know. =0(

    ReplyDelete
  3. Hi Bonnie,

    In this your useful example, what content (probably it's your base,main...classes) there are in: MyBaseForm, MyBiz classes?

    Thanks

    ReplyDelete
    Replies
    1. Thanks for commenting. I have always started out with sub-classes of the native base controls, even if they end up not doing anything, because it's easier to add functionality to them later as you need to. You use your sub-classed controls on a Form (and as a Form) instead of the native controls (like TextBox, ComboBox, etc.).

      The following link is a short post, but you can see what I'm talking about here:
      https://geek-goddess-bonnie.blogspot.com/2009/10/why-and-how-to-sub-class-base-classes.html

      I haven't done any UI type of applications in quite some time now (I've been working on back-end server stuff now for years), but an example of what might be useful in a base class such as MyBaseForm, might be something like this: say you want to prompt the user to ask if they want to save their changes, when they attempt to close any Form in your application. And also make it easy for the sub-classes of MyBaseForm to determine if there have been changes to Save, if they need to.

      public class MyBaseForm : System.Windows.Forms.Form
      {
      #region Declarations

      public bool ShowMaximized { get; set; }

      #endregion

      #region Constructor
      public MyBaseForm()
      {
      this.Closing += new System.ComponentModel.CancelEventHandler(this.ClosingHandler);
      this.Load += new System.EventHandler(this.MyBaseForm_Load);
      }
      #endregion

      #region Methods
      /// You will want to override AskToSave() to do additional things before you ask for confirmation
      /// such as checking if there are any changes at all, and then saving them if you receive an OK
      protected virtual DialogResult AskToSave()
      {
      return this.ConfirmSave();
      }
      protected virtual DialogResult ConfirmSave()
      {
      string title = this.Text;
      string message = "Do you wish to save your changes?";
      return MessageBox.Show(message, title, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
      }
      #endregion

      #region Events
      protected virtual void ClosingHandler(object sender, System.ComponentModel.CancelEventArgs e)
      {
      if (this.AskToSave() == DialogResult.Cancel)
      e.Cancel = true;
      }
      private void MyBaseForm_Load(object sender, System.EventArgs e)
      {
      if (this.ShowMaximized)
      this.WindowState = FormWindowState.Maximized;
      }
      #endregion
      }

      Now, your sub-class of MyBaseForm might look like this:

      public class CustomerForm : MyBaseForm
      {
      #region Declarations

      private DataSet dsCustomer;

      #endregion

      #region Constructor
      public MyBaseForm()
      {
      this.Load += new System.EventHandler(this.CustomerForm_Load);
      }
      #endregion

      #region Methods
      protected override DialogResult AskToSave()
      {
      DialogResult result = DialogResult.Ignore;
      if (this.dsCustomer.HasChanges())
      {
      result = this.ConfirmSave();
      if (result == DialogResult.OK)
      {
      // put your code here for saving your Customer data
      // probably by making calls to a Biz or DataAccess class
      }
      }
      return result;
      }
      #endregion

      #region Events
      private void CustomerForm_Load(object sender, System.EventArgs e)
      {
      this.dsCustomer = new DataSet();
      // Include code here for filling your DataSet (Biz or DataAccess calls)
      // and anything else you need to do, such as databinding your Form
      // controls to the dsCustomer
      }

      #endregion
      }

      Delete
    2. Thanks for the explanation.
      This is very nice and clear.

      Thank you

      Delete
  4. Hi Bonnie,

    The BaseForm has nothing to do with the BizBase, for example it don't use generic BizBase class?

    Thanks

    ReplyDelete
    Replies
    1. Hi, thanks for commenting, sorry for the very late reply. Three months overdue!

      I hope I'm understanding your question correctly. I'm not sure if you're talking about the original post, or the reply I made in the Comments that had an example of a MyBaseForm class.

      In this simple example, MyBaseForm is useful mainly for UI-related common things, like maybe a user-prompt when closing a Form (a protected virtual method that could be overridden in your sub-class if you needed to, but basic functionality of that method would have code that would work even if you didn't override it), and things like that.

      You can obviously make it more robust, by adding other properties and methods. For example, you could use an Interface, IBizBase, and have a property in the MyBaseForm based just on IBizBase (which will be then be used in all your Forms that sub-class from MyBaseForm):

      public interface IBizBase
      {
      DataSet GetData();
      bool SaveData(DataSet ds);
      }

      // Now the classes that implement and use this Interface
      public class MyCustomerBiz : IBizBase
      {
      public NorthwindCustomerOrdersDataSet dsCustomer { get; set; }

      public DataSet GetData()
      {
      this.dsCustomer = new NorthwindCustomerOrdersDataSet();

      // Make a call to your DataAccess class to retrieve data
      // and put it into the dsCustomer DataSet.

      return this.dsCustomer;
      }
      public bool SaveData(DataSet ds)
      {
      bool IsOK = true;

      // Make a call to your DataAccess class to save your data
      // return true or false, depending on whether it was successfully saved

      return IsOK;
      }
      }

      public abstract class MyBaseForm : System.Windows.Forms.Form
      {
      protected IBizBase oBiz { get; set; }

      // This class would obviously have a lot more properties and methods
      // which I'm not going to go into now. There is an example of more stuff in the
      // comment I made above in the blog post.
      // Here, I'm just showing the use of an IBizBase interface.
      }
      public class MyCustomerForm : MyBaseForm
      {
      protected NorthwindCustomerOrdersDataSet dsCustomer;

      public MyCustomerForm()
      {
      this.oBiz = new MyCustomerBiz();
      }

      public void GetAndProcessData()
      {
      this.dsCustomer = (NorthwindCustomerOrdersDataSet)this.oBiz.GetData();

      // continue with whatever you need to do with the data
      }
      public bool SaveData()
      {
      return this.oBiz.SaveData(this.dsCustomer);
      }
      }

      I hope this helps!

      Delete
    2. Hi Bonnie,

      Thanks for answer .
      Yes Bonnie, you are understand my question correctly.
      Your explanation help me :)

      Thanks

      Delete
    3. Great! I'm glad I could help!! =0)

      Delete