DB is inside - DemoCustomerFrms\DB\Raptor_Core_Local_DB_Backup_17_11_2010.
Check - Rocket Framework (.NET 4.0) to see how this is being used in practise.
Table of Contents
- Introduction
- Background
- How You Use a Framework?
- Let's Have a Look at Our Demo Framework
- How We Build Our Framework Gradually?
- High Level Design Overview
- Let's look at the ‘Demo.Framework’ Module Design
- Have a Look at the ‘BaseGridView’ Code Below
- Let's look at the ‘Demo.Data’ Module Design
- Let's look at the ‘Demo.Core’ Module Design
- Let's look at the ‘Demo.Core.UserControls’
- Module Design Implementation of Data Controls
- Final Note
- History
Introduction
Web system designing is well supported with many online materials. In contrast to that, support materials available for Windows-Forms based (desktop applications) system designing is less. In 1980s, during the era of two-tier systems,
desktop applications ruled the software world. Now the time has changed and software geeks are now focused on web systems, leaving developers to design desktop applications at their will. Today you hardly see any architectural discipline being adopted in desktop applications. Therefore I thought of shedding some light over this ignored sector to improve the designing aspect of desktop applications.
In this attempt, I will demonstrate architecting a simple desktop application. Additionally, I will also develop a reusable
framework that you can use to develop any desktop application.
When I talk about ‘framework’, you may have thought what a framework really is. I see there are many misinterpretations around the term ‘framework’. So I will give you an introduction to ‘framework’ too. That I will do at the early part of this write-up. But even before that, let me tell you how the idea to write this article started.
Background
We, in our company, are developing a large Windows Forms based application on .NET
Framework 4.0 using Visual Studio 2010 these days. In that, we had the need to develop an administrative user interface (to do CRUD operations of master records), where users with certain rights can directly edit certain elements of the database tables. So we needed a set of screens, where they can edit the records of DB tables. One such screen is
shown below for your reference. You may develop this screen in a matter of 20 minutes using our 'framework'.
During the initial stages of this project, as usual, I spent a few days researching to find the right mix of technologies and design approaches to form the design of this system. I thought of developing a generic
framework having the main goal to support editing of DB records. That framework
was planned in such a manner that we can use that as the basis for any 'Desktop Application' development. The main goal of this writing is to introduce that ‘framework’ to
the CodeProject community.
I thought that the system architecture not only should support editing database tables directly but also support dealing with custom business objects that use many fields from different tables too. Developing a generic 'framework' to support this is nearly impossible as different domains have different business needs. Therefore I thought that it is important to introduce multiple ‘wrapping’ layers on top of the generic framework to support handling domain specific needs. In other terms, this means we need
a domain specific framework on top of the generic framework. That is to implement unique domain specific features of systems. So going forward, there will be several
domain specific frameworks adding to this library. Additionally, to support customers' very specific highly customized unique requirements, I planned
to have another layer on top of both the generic framework and the domain specific
framework.
If everything goes well, once this 'architecture’ is stable, when a new customer comes, we just have to get a copy of the generic
framework and the respective domain specific framework from our software store and spend
a few days adding customer specific features to it before bundling everything to form the beta version of the release.
Doesn't that sound exciting?
How to Use a Framework?
Let’s examine the picture above. It is a framework and fairly easily you can form a room in it. Then with
a little more work you can extend it to form a complex of rooms. Then you can develop
a few extensions to it to form either a 'Motel', 'Hotel', or a luxury apartment complex. This is how you use a framework to build something quicker. This very same concept is what we are going to use to build our software system too.
This concept of building on a framework is not new. Even the language you use, .NET/C#, is a
framework. Not only that, .NET has newer frameworks built on top of older frameworks too. In the next part of the article you will see how
the .NET Framework 3.0 is being built on top of .NET Framework 2.0.
Let's Have a Look at Our Demo Framework
The picture shows how Microsoft has built .NET Framework 3.0 on top of .NET Framework 2.0. This shows how
a framework can be used to build another on top of it.
As in the picture above, we will be building our framework on top of .NET Framework 4.0. That is the latest version of the .NET Framework family.
How We Build Our Framework Gradually?
This shows a strategy that you can use to develop a ‘framework’ without spending too much of your time initially. Firstly you need to have
the guts to say ‘NO’ to the piles of the ever growing 'What if' type requirements that people around you want to add to the ‘framework’. You need to define the boundaries of the
framework. Then you need to have some strategies in place so that you can build this system without a waste of your time.
As you can see in the picture, you have to identify a set of core features for the main
framework. Then let the domain specific framework auto-build through every custom implementation.
High Level Design Overview
This is the package diagram of our system. I have used some color coding to make it easier for you to understand.
As you can see, there are a few modules being used to developed this system. You can see the generic 'Demo.Framework' given right at the top. Then by using that, the core business logic as well as the user control libraries are formed. ‘Demo.Common’ is common for all and something that we will be sharing across multiple (all) packages. The data access layer (Demo.Data) library is developed using Microsoft ADO.NET Entity Framework.
Let's Look at the ‘Demo.Framework’ Module Design
Enough of talking, let's jump right into the water now. Don't be afraid, I know for a fact that some people are born afraid of system architecture. I too had it for ‘Chemistry’. I know for a fact that I didn’t have the right start to it. In
the software world too, I have seen overly designed systems with never ending class hierarchies. Some of these systems are far more complicated for their requirements. But here in our design, I have kept it very simple. I wanted everyone to understand, be able to maintain, and also upgrade the system easily.
This design has a few but valuable 'interfaces'. Do not think that these interfaces make the design look complex or elegant, rather I have added them with valid reasons and to achieve certain things that wouldn’t have been possible otherwise. I will explain those design moves later in this article.
There are seven class interfaces that are being used here. As you may have heard, in general
an ‘interface’ separates the definition of an object from its implementation. If you think of an interface as a contract, it is clear that both sides of the contract have a role to play. The publisher (or the framework side of the equation) of the interface agrees never to change that interface, and the implementer (or the custom implementation side of the equation) agrees to implement the interface exactly as it was designed. This helps using the interface across many other generic classes even without knowing their implementation.
-
IHandler<V>
- This is the generic interface that is used to implement all handler classes.
Classes of type ‘Handler’ are the ones that are used to implement handlings for all business objects of this system.
-
IMapper<V, D>
- This wraps a third party library called Auto-Mapper. The Auto-Mapper (ref:
http://automapper.codeplex.com/) uses a convention-based matching algorithm to match up
a source object to its destination object values.
-
IObject
– This interface is what implements the abstract BaseObject
which
is used as the base class for all business objects.
Note: V
stands for the view or business object whereas D
stands for data objects or a table of the database.
-
IDataContainer<V>
- This is what is used to implement
BaseData<V>
, the generic user control. I must tell you that Microsoft is still not ready to support applying object oriented programming concepts for user controls or developing generic user controls. The
IDE starts giving you trouble immediately after it sees the ‘<’ mark. The beauty of the generic implementation made me ignore that hassle. As you are following my directions you will also have to face troubles
that the Visual Studio IDE (I saw this bug was reported around 2003 but is still there in VS 2010 too)
is going to throw at you.
-
IGridView
– This is the interface that is used to implement the generic
BaseGridView<H, V>
. In this H
is of type
IHandler<V>
whereas V
is of type BaseObject
.
This framework is elegantly designed with minimum complexity. I know that some of you still may find it difficult to understand. If you don’t have expertise dealing with generics, interfaces, etc., then just find the pattern to using this
framework and start using it, then slowly you will understand it. Even today there are many developers take advantage of this to build their applications without having a clue about the design of the
framework or generics or interfaces. Once understood, the pattern of the design itself will guide you to use it.
Have a look at the ‘BaseGridView’ code below
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Demo.Framework
{
public abstract partial class BaseGridView<H, V> : BaseControl, IGridView
where H : IHandler<V>
where V: BaseObject
{
public event DataGridViewCellEventHandler DataRowSelected;
public BaseGridView(H h)
{
InitializeComponent();
_hadler = h;
}
private H _hadler;
public virtual void LoadData()
{
this.DataList = _hadler.GetAll();
}
private V _selectedItem;
public V SelectedItem
{
get { return _selectedItem; }
}
private List<V> _originalList = null;
private List<V> _dataList;
public List<V> DataList
{
get { return _dataList; }
set
{
_dataList = value;
this.dataGridViewMain.DataSource = null;
if (_dataList == null) return;
this.dataGridViewMain.Columns.Clear();
this.dataGridViewMain.AutoGenerateColumns = true;
this.dataGridViewMain.DataSource = this._dataList;
OnHideColumns(ref dataGridViewMain);
if (_dataList.Count > 0)
{
int cCount = this.dataGridViewMain.Columns.Count;
foreach (KeyValuePair<string, object> item in _dataList[0].Data)
{
DataGridViewTextBoxColumn mRef = new DataGridViewTextBoxColumn();
mRef.HeaderText = item.Key;
this.dataGridViewMain.Columns.Add(mRef);
}
for (int i = 0; i < dataGridViewMain.RowCount; i++)
{
int c = cCount;
IObject boundItem = (V)dataGridViewMain.Rows[i].DataBoundItem;
foreach (KeyValuePair<string, object> item in boundItem.Data)
this.dataGridViewMain[c++, i].Value = item.Value;
}
}
}
}
protected virtual void OnHideColumns(ref DataGridView dataGridView)
{
dataGridView.Columns["Id"].Visible = false;
dataGridView.Columns["Data"].Visible = false;
dataGridView.Columns["SourcePoco"].Visible = false;
}
public string Heading
{
get { return groupBoxMain.Text; }
set { groupBoxMain.Text = value; }
}
protected List<V> Search(string query)
{
List<V> list = new List<V>();
string[] qItems = query.Split(' ');
foreach (V item in _dataList)
{
for (int i = 0; i < qItems.Length; i++)
if (item.FoundIt(qItems[i]))
list.Add(item);
}
return (list.Count > 0) ? list : null;
}
private void buttonSearch_Click(object sender, EventArgs e)
{
if (_originalList == null)
{
_originalList = _dataList;
return;
}
string s = this.textBoxSearch.Text.Trim();
this.dataGridViewMain.Columns.Clear();
if (string.IsNullOrEmpty(s))
this.DataList = _originalList;
else
this.DataList = this.Search(s);
}
private void dataGridViewMain_RowEnter(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
_selectedItem = (V)dataGridViewMain.Rows[e.RowIndex].DataBoundItem;
if (DataRowSelected != null)
DataRowSelected(_selectedItem, e);
}
private void buttonRefresh_Click(object sender, EventArgs e)
{
LoadData();
this.textBoxSearch.Text = "";
}
}
}
I want you to focus on the top part of this generic abstract class named
BaseGridView<H>
. In the implementation, the constructor is being used to get the implemented version of the handler (H
) passed on to it. Since all handlers of our system
implement IHandler
, the system has the advantage of using the built-in methods of handlers via the
IHandler
interface. Interestingly we do this even without knowing the actual implementation of it. This is a very interesting part of our code. You will be able to understand this design approach more as you read along this article.
At the same time I invite you to carefully examine the source code too.
BaseHandler<V, D>
- This abstract class implements both
IHandler
and IMapper
interfaces. It contains the generic implementation for handling business objects. All these classes and interfaces use two generics namely
V
and D
of type BaseObject
and EntityObject
. The
BaseHandler
associate with the IRepository<D>
interface too. That association is used to hide
the repository implementation. The repository object which passes on to this at the time of implementing
a specific handler is used to retrieve data related to a particular data object from the database. There the Auto-Mapper is used to map them from
D
of type EntityObject
to V
of type BaseObject
and pass that on to the next layer. The implementation of IRepository<D>
is done under
the Demo.Data library. I will explain that part later.
The base classes help us to do the generic implementation once and reuse them with multiple types.
Let's Look at the ‘Demo.Data’ Module Design
The system uses the ADO.NET Entity Framework (.NET 4) to generate the data access layer (ref: http://msdn.microsoft.com/en-us/data/ee712907.aspx). I also went on to use
a T4 template, but later found that the special generic repository implementation that I am introducing here is not easily doable with
a T4 template. So I just use the default code that Entity Framework generates. For your understanding, have a look at the generic repository code below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
using System.Linq.Expressions;
using System.Data;
using System.Data.Objects.DataClasses;
using System.Data.Common;
using Demo.Framework;
namespace Demo.Data
{
public sealed class DemoRepository<T> : IRepository<T>
where T : EntityObject
{
static readonly DemoRepository<T> instance
= new DemoRepository<T>(new DemoCoreEntities());
private static readonly Object _lockObject = new Object();
static DemoRepository()
{
if (_lockObject == null)
_lockObject = new Object();
}
public static DemoRepository<T> Instance
{
get
{
return instance;
}
}
DemoRepository(ObjectContext repositoryContext)
{
_repositoryContext = repositoryContext ?? new DemoCoreEntities();
_objectSet = _repositoryContext.CreateObjectSet<T>();
}
private static ObjectContext _repositoryContext;
private ObjectSet<T> _objectSet;
public ObjectSet<T> ObjectSet
{
get
{
return _objectSet;
}
}
#region IRepository Members
public IRepository<T> GetRepository()
{
return Instance;
}
public void Add(T entity)
{
lock (_lockObject)
{
this._objectSet.AddObject(entity);
_repositoryContext.SaveChanges();
_repositoryContext.AcceptAllChanges();
}
}
public void Update(T entity)
{
lock (_lockObject)
{
_repositoryContext.ApplyOriginalValues(((IEntityWithKey)entity)
.EntityKey.EntitySetName, entity);
_repositoryContext.Refresh(RefreshMode.ClientWins, _objectSet);
_repositoryContext.SaveChanges();
_repositoryContext.AcceptAllChanges();
}
}
public void Delete(T entity)
{
lock (_lockObject)
{
this._objectSet.DeleteObject(entity);
_repositoryContext.Refresh(RefreshMode.ClientWins, _objectSet);
_repositoryContext.SaveChanges();
_repositoryContext.AcceptAllChanges();
}
}
public void DeleteAll()
{
_repositoryContext
.ExecuteStoreCommand("DELETE " + _objectSet.EntitySet.ElementType.Name);
}
public IList<T> GetAll()
{
lock (_lockObject)
{
return this._objectSet.ToList<T>();
}
}
public IList<T> GetAll(Expression<Func<T, bool>> whereCondition)
{
lock (_lockObject)
{
return this._objectSet.Where(whereCondition).ToList<T>();
}
}
public T GetSingle(Expression<Func<T, bool>> whereCondition)
{
lock (_lockObject)
{
return this._objectSet.Where(whereCondition).FirstOrDefault<T>();
}
}
public IQueryable<T> GetQueryable()
{
lock (_lockObject)
{
return this._objectSet.AsQueryable<T>();
}
}
public long Count()
{
lock (_lockObject)
{
return this._objectSet.LongCount<T>();
}
}
public long Count(Expression<Func<T, bool>> whereCondition)
{
lock (_lockObject)
{
return this._objectSet.Where(whereCondition).LongCount<T>();
}
}
#endregion
}
}
The code you see above is a special one. I am trying to use the 'Generic' repository as effectively as possible. As you can see, the repository
is implemented as a single object. The thread safe version of the Singleton design pattern is used to implement the repository. In addition to that, an additional protection is done using
_lockObject
to allow concurrent access to the repository. Just to demonstrate the usage of SQL commands directly inside the repository,
the DeleteAll()
method is written.
Let's Look at the ‘Demo.Core’ Module Design
‘Demo.Core’ is used to implement all business objects and their handling functions. Going along with
the MVC architecture, I thought of naming all business objects with the ‘View’ suffix. So in the library you will find two types of classes. One is of type handler (which is a variant of
the Service
class of the MVC architecture) and the other is of View type. Handler type classes have all operations related to a particular view object. Let’s open the
HandlerApplicationUser
class and see what we find inside it.
The class HandlerApplicationView
is as follows. It is pretty much an empty class. One requirement is to pass the correct instance of the repository to its base class. In addition to that if you want to do special mappings from
the ‘data object’ to the ‘business object’ or vice versa, then you need to update the mapping expression by using either
the ForwardMap
or BackwardMap
method.
E.g., for BackwarMap
: If you have ‘FirstName’, ‘Initials’, and ‘LastName’ in one table, then the respective data object will have three fields with
the same names. If you want these three fields of the dataobject to be mapped to a new field called
Name
of the business object then you can use the provided BackwardMap
mapping expression. As you can see, the method
BackwardMap
has IMappingExpression<ApplicationUser, ApplicationUserView>
,
which has, in our case, detail to map ApplicationUser
to ApplicationUserView
.
Are lambda expressions bothering you? That is not difficult at all, you just have to understand the pattern; once understood, there is nothing difficult about lambda expressions.
The LogPrefix
property finds the handle class below is for log4net. If you look at
the LoggerBase
class inside the Common folder of the framework library, where you will understand that ‘Logger Base’ wraps log4net functions to gain flexibility to replace it with another.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Demo.Data;
using AutoMapper;
using Demo.Core.Models;
using Demo.Framework;
namespace Demo.Core
{
public class HandlerApplicationUser : BaseHandler<ApplicationUserView, ApplicationUser>
{
public HandlerApplicationUser()
: base(DemoRepository<ApplicationUser>.Instance)
{
}
public override void ForwardMap(IMappingExpression<ApplicationUserView, ApplicationUser> mappingExpression)
{
mappingExpression
.ForMember(dest => dest.ApplicationUserId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Gender, opt => opt.MapFrom(src => (src.SelectedGender.Key == 1) ? true : false))
.ForMember(dest => dest.AdditionalData, opt => opt.MapFrom(src => (src.Data != null) ? src.Data.ToString() : ""));
}
public override void BackwardMap(
AutoMapper.IMappingExpression<ApplicationUser, ApplicationUserView> mappingExpression)
{
mappingExpression
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.ApplicationUserId))
.ForMember(dest => dest.SelectedGender, opt => opt.MapFrom(src =>
(src.Gender) ? new KeyValuePair<int, string>(1, "Male") :
new KeyValuePair<int, string>(2, "Female")))
.ForMember(dest => dest.SourcePoco, opt => opt.MapFrom(src => src))
.ForMember(dest => dest.Data, opt =>
opt.MapFrom(src => AdditionalData.Resolve(src.AdditionalData)));
}
protected override Type LogPrefix
{
get { return this.GetType(); }
}
}
}
Let's look at the ‘Demo.Core.UserControls’ module design
This is where we develop all user controls. There again, as I said before, I am using generics extensively. But the design approach is pretty much like as it was with
the above modules. So I will leave it up to you to look at the source code and understand it.
ApplicationUserData
– This is a user control created by
extending BaseData
. This is where you will have textboxes, dropdowns, and labels allowing viewing, editing of business objects. There are
a few steps that you need to follow when creating such a data control.
-
Create the business object (name it ApplicationUserView).
-
Create the handler class to handle the business object (name it HandlerApplicationUser).
-
Add a new ‘Data Source’ by choosing ‘Object’ as the source (look at the screen below, you need to click on 'New Data Source' here). That will let you choose objects that can be used to generate the required data-bound control. You need to find the correct class library first and then select the right business object to create this new ‘Data Source’ (in our case, I have picked 'ApplicationUserView' as the source object).
-
Add a new user control giving a standard name like <name of the business object>Data.cs (in our case, it was 'ApplicationUserData').
-
Drag the whole view on to the user control and delete the ones that you don't want the user to view/edit on to the user control.
-
Navigate to the source code of this newly added user control and change the parent class from “
: UserControl
” to “: BaseData<name of the business object>
” (in our case it was
BaseData<ApplicationUserView>
). This change will make you override three methods as explained in
the “Implementation of Data Controls” section below.
Note: Editing of these user controls using the IDE is not possible as visual studio does not understand generic type user controls. If you want to edit the data controls after they are built then you need to comment all three overridden methods and change the parent class back to “UserControl”. This will allow you to use the IDE for control editing. Once the editing is completed you need to role the changes back to their originals.
ApplicationUserDataGridView
– This user control is capable of listing down a set of business objects. It also has a generic search. You can control the name of the columns of the grid by using
DisplayNameAttribute
. Other than that this has nothing much for the one which uses it to do. You can check the source code to understand
the various things possible to be done here. One thing to note here is that you can use the method name
OnHideColumns
if you want to hide a particular column just before displaying.
ApplicationUserManager
– This is the main manager control that helps you do all CRUD operations related to a business object. You just have to pass
a few parameters up to the base class and that will take care of the rest for you.
Implementation of Data Controls
If you have to write any code in this library that is only inside the corresponding
data control. I have given you one such implementation below.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Demo.Framework;
using Demo.Core.Models;
using Demo.Common;
namespace Demo.Core.Controls.ChildControls
{
public partial class ApplicationUserData : BaseData<ApplicationUserView>
{
public ApplicationUserData()
{
InitializeComponent();
}
public override void SetBindingSource(ref BindingSource DemoBindingSource)
{
DemoBindingSource = this.applicationUserViewBindingSource;
}
public override ApplicationUserView MapIt(int Id)
{
ApplicationUserView apView = this.BindedView;
apView.Id = Id;
apView.UserName = this.userNameTextBox.Text;
apView.Password = this.passwordTextBox.Text;
apView.CustomerId = NullHandler.ConvertToInt(this.customerIdTextBox.Text);
apView.SelectedGender = (KeyValuePair<int, string>)this.genderCombo1.SelectedItem;
apView.FirstName = this.firstNameTextBox.Text;
apView.ShortName = this.shortNameTextBox.Text;
apView.MiddleName = this.middleNameTextBox.Text;
apView.LastName = this.lastNameTextBox.Text;
apView.StatusTypeId = NullHandler.ConvertToInt(this.statusTypeIdTextBox.Text);
apView.LastLoginDateTime = this.lastLoginDateTimeDateTimePicker.Value;
apView.Email = this.emailTextBox.Text;
apView.Phone = this.phoneTextBox.Text;
return apView;
}
protected override void _DemoBindingSource_DataSourceChanged(object sender, EventArgs e)
{
if (this.BindedView != null)
this.genderCombo1.SelectedItem = this.BindedView.SelectedGender;
}
}
}
Final Note
The demonstration provided here is "as is" without warranty of any kind. I make no warranties, express or implied, that the source code given here is free of error or that they will meet your requirements for any particular application. It is up to you to evaluate, learn, review, and use it as applicable.
History