![]() |
Web Development »
ASP.NET »
General
Intermediate
License: The Code Project Open License (CPOL)
Catharsis Tutorial 03 - Rapid Application Development using CatharsisBy David O'SullivanStep by step guide to building a robust enterprise level multi-tier ASP.NET MVC web application using Catharsis to automatically generate the code infrastructure |
C#, .NET (.NET3.5), ASP.NET, MVC, CEO, Architect, DBA, Dev, Design
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
Catharsis is a powerful RAD (Rapid Application Development) tool which is built around a solid architecture using ASP.NET MVC and NHibernate. Best practice design patterns and separation of concerns between layers were key factors in the design of the Catharsis framework.
Using Guidance, the framework offers RAD functionality on a robust enterprise level architecture by automatically generating most of the code you need for your web application entities. Filling in a few simple dialog screens is all that is required in many cases.
This article explains how you can quickly build an application to create, read, update and delete entities (C.R.U.D.). The Catharsis Guidance automatically generates the multi-tier architecture and adds a skeleton infrastructure of classes and interfaces which will work without much additional coding. The article builds on the previous one in this series which examined the Catharsis example project. In this article we will add a new entity to that example project which is available to download. This information will allow you to quickly create your own CRUD application.
In addition to creating simple entities this article will also explain how to use the framework to code references between entities, for example where one entity is used as an entity-type in another entity. Finally we will look at how to add business rules to your application.
If you have a database and want to quickly create a robust enterprise level web application to access that database Catharsis offers the best way to achieve this.
Unlike many frameworks Catharsis was written using public and protected methods which makes it completely extensible. The programmer can take control of their own application and override methods when they want to add new functionality when they need to. This is not necessary when creating a lot of applications on Catharsis but for enterprise level applications it is nice to know that the option is available if needed.
Before reading this article please read the Catharsis installation guide, this is Catharsis Tutorial 01 in the list available here: http://www.codeproject.com/script/Articles/MemberArticles.aspx?amid=4013680
A powerful example solution based on the Catharsis framework is available for download. The example solution is called Firm.SmartControls and can be downloaded here: http://catharsis.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=28510
The second article in this series looks into the example solution and explains how it is set up, read this before continuing, (Catharsis Tutorial 02): http://www.codeproject.com/script/Articles/MemberArticles.aspx?amid=4013680
A good way to lean more about Catharsis is to install the demo solution and follow the step by step guide in this article to add a new entity to that solution.
A printable Word version of this article is availeble: Download Catharsis_Tutorial_03_-_RAD_using_Catharsis.zip - 346.77 KB
The solution contains entities called Agent and AgentContract, we will add an additional one called Client.
Firm.SmartControls solution actually contains two tables which are not yet mapped so we will use one of these as an example of how to add a new entity.




Code, Name and BankCode. Guidance will automatically generate checks to ensure that Code is unique (this can be deleted if it is not required). Adding properties here reduces the amount of work that we will have to do later, however we can only add value types, for example a string property ‘name’, we cannot add Entity types, for example a foreign key which references another table such as Country.

Str.Controller.cs file and add the highlighted line:





<?xml version='1.0' encoding='utf-8' ?>
<hibernate-mapping xmlns='urn:nhibernate-mapping-2.2'
namespace='Firm.SmartControls.Entity.Insurance' assembly='Firm.SmartControls.Entity'>
<class name='Client' table='Client' lazy='true' >
<id name='ID' column='ClientId' >
<generator class='native'></generator>
</id>
<property not-null='true' name='Code' />
<property not-null='true' name='Name' />
<property not-null='true' name='BankCode' />
</class>
</hibernate-mapping>
InsuranceClient, not Client.InsuranceClientId, not ClientId.<?xml version='1.0' encoding='utf-8' ?>
<hibernate-mapping xmlns='urn:nhibernate-mapping-2.2'
namespace='Firm.SmartControls.Entity.Insurance' assembly='Firm.SmartControls.Entity'>
<class name='Client' table='InsuranceClient' lazy='true' >
<id name='ID' column='InsuranceClientId' >
<generator class='native'></generator>
</id>
<property not-null='true' name='Code' />
<property not-null='true' name='Name' />
<property not-null='true' name='BankCode' />
<many-to-one name='Country' column='CountryId' lazy='false' ></many-to-one>
<many-to-one name='Gender' column='GenderId' lazy='false' ></many-to-one>
</class>
</hibernate-mapping>
Firm.SmartControls.Entity.Insurance in the file above so this will need to be changed to reflect the changes we made in the mapping file.
Code, Name and BankCode. We will now add Gender and Country. These are CodeList objects so we need to add a using directive for Firm.SmartControls.Entity.CodeLists in order for the Gender and Country datatypes to be recognised. The code we should add is in bold.using System; // ===================================
using System.Collections.Generic; // Guidance generated code © Catharsis
using System.Linq; // ===================================
using ProjectBase.Core;
using ProjectBase.Core.PersistentObjects;
using ProjectBase.Core.Collections;
using Firm.SmartControls.Entity.CodeLists;
namespace Firm.SmartControls.Entity.Insurance
{
/// <summary>
/// Entity Client.
/// </summary>
[Serializable]
public class Client : Persistent
{
public virtual string Code { get; set; }
public virtual string Name { get; set; }
public virtual string BankCode { get; set; }
/// <summary>
/// codelist
/// </summary>
public virtual Gender Gender { get; set; }
/// <summary>
/// codelist
/// </summary>
public virtual Country Country { get; set; }
Firm.SmartControls.Data.Insurance.ClientDao)Gender and Country should be available in intellisense when we add the two new entries, this is obviously because they are now properties of the Client entity.

Code, Name and BankCode) are automatically added.
Gender and Country properties.ClientDetailsWC.ascx file (The abbreviation WC is for ‘Web Control’)
Gender and Country.<div class='newRow mh50'>
<fieldset class='newDetail w32p'>
w32p represents a CSS class for width. We can examine these CSS classes in the following file:
<fieldset class='newDetail w32p'>
<legend><%= GetLocalized(Str.Business.CodeLists.CodeList)%></legend>
<div class='fieldset'>
<div class='inputWC inputWC60 w100p'>
<div class='label'><%= GetLocalized(Str.Controllers.Country)%></div>
<div class='input'><% Country.SetEntityAsDataSource(Model.Item.Country); %>
<smart:AsyncComboBoxWC ID='Country' runat="'server'" TargetController='Country' /> </div>
</div>
<div class='inputWC inputWC60 w100p'>
<div class='label'><%= GetLocalized(Str.Controllers.Gender)%></div>
<div class='input'><% Gender.SetEntityAsDataSource(Model.Item.Gender); %>
<smart:AsyncComboBoxWC ID='Gender' runat="'server'" TargetController='Gender' /> </div>
</div>
</div>
</fieldset>




CTRL key and typing m o will expand all the regions, likewise CTRL m l will collapse all the regions.OnAfterBindModel and OnAfterBindSearch are implemented in the default Controller. These two are used to take care of entity-type properties so we will use these to add Country and Gender.
ClientController in our case. It is also used to load entities which we may need if they are entity types in our entity. This will be the case in our example because we will have two entity types. These objects use the ‘lazy load’ approach, this means that they are only created when they are needed, this improves efficiency in the application.OnAfter region to handle the entity types/// <summary>
/// Binds non value type properties for an Item
/// </summary>
/// <returns></returns>
protected override bool OnAfterBindModel()
{
var success = base.OnAfterBindModel();
int id = 0;
// Country
if (Request.Form.AllKeys.Contains(Str.Controllers.Country)
&& int.TryParse(Request.Form[Str.Controllers.Country], out id))
{
Model.Item.Country = CountryFacade.GetById(id);
}
// Gender
if (Request.Form.AllKeys.Contains(Str.Controllers.Gender)
&& int.TryParse(Request.Form[Str.Controllers.Gender], out id))
{
Model.Item.Gender = GenderFacade.GetById(id);
}
return success;}
/// <summary>
/// Binds non value type properties for searching
/// </summary>
/// <returns></returns>
protected override bool OnAfterBindSearch()
{
var success = base.OnAfterBindSearch();
int id;
// Country
if (Request.Form.AllKeys.Contains(Str.Controllers.Country)) // there was some selection
{
Model.SearchParam.Example.Country = null; // clear previous to null (it could be final stage also)
if (int.TryParse(Request.Form[Str.Controllers.Country], out id))
{
Model.SearchParam.Example.Country = CountryFacade.GetById(id);
}
}
// Gender
if (Request.Form.AllKeys.Contains(Str.Controllers.Gender)) // there was some selection
{
Model.SearchParam.Example.Gender = null; // clear previous to null (it could be final stage also)
if (int.TryParse(Request.Form[Str.Controllers.Gender], out id))
{
Model.SearchParam.Example.Gender = GenderFacade.GetById(id);
}
}
return success;
}
Country was provided on the form (in the ascx control) and also to ensure that the supplied value is an integer. Then we call the CountryFacade to find the Country which has the id which was sent from the form and the Country object is returned and added to the Client object.public override string ControllerName { get { return Str.Controllers.Client; } }
/// <summary>
/// Allowes LAZILY (or via IoC) to work with Country
/// </summary>
public virtual ICountryFacade CountryFacade
{
protected get
{
if (_countryFacade.IsNull())
{
_countryFacade = FacadeFactory.CreateFacade<ICountryFacade>(Model.Messages);
}
return _countryFacade;
}
set
{
Check.Require(value.Is(), " ICountryFacade cannot be null");
_countryFacade = value;
}
}
/// <summary>
/// Allowes LAZILY (or via IoC) to work with Gender
/// </summary>
public virtual IGenderFacade GenderFacade
{
protected get
{
if (_genderFacade.IsNull())
{
_genderFacade = FacadeFactory.CreateFacade<IGenderFacade>(Model.Messages);
}
return _genderFacade;
}
set
{
Check.Require(value.Is(), " IGenderFacade cannot be null");
_genderFacade = value;
}
}
OnAfter methods above #region members
IGenderFacade _genderFacade;
ICountryFacade _countryFacade;
#endregion members


Gender and Country do not appear in the list. The properties of the client entity which do appear are the ones which we provided to the Guidance dialogs when we were creating the web infrastructure. As mentioned above the OnList region in the control should be expanded to handle this.ClientController to show the Gender and Country of the listed Client entities.protected override void OnListToDisplay()
{
Model.ListModel.ItemsToDisplay = Facade.GetBySearch(Model.SearchParam)
.Select(i => new ItemToDisplay()
{
ID = i.ID,
Description = i.ToDisplay(),
Items = new List<IHeaderDescription>
{
new HeaderDescription { HeaderName = "Code", Value = i.Code},
new HeaderDescription { HeaderName = "Name" , Value = i.Name },
new HeaderDescription { HeaderName = "BankCode" , Value = i.BankCode },
new HeaderDescription { HeaderName = Str.Common.ID , Value =
i.ID.ToDisplay(), Align = Align.right },
}
} as IItemToDisplay);
}
new HeaderDescription { HeaderName = Str.Controllers.Country, Value = i.Country.Code, SortByObject=Str.Controllers.Country, SortByProperty=Str.Common.Code},

ClientController in this case) and press F5 or click the debug button only the files (DLLs) which Visual Studio thinks need to be updated will be updated. Because there is no references exist between the Controller and the web project. This will be explained in more detail later but remember that if you expect to see changes rebuild the entire solution before you test your changes.


<fieldset class='newDetail w30p'>
<legend><%= GetLocalized(Str.Business.CodeLists.CodeList)%></legend>
<div class='fieldset'>
<div class='inputWC inputWC60 w100p'>
<div class='label'><%= GetLocalized(Str.Controllers.Country)%></div>
<div class='input'><% Country.SetEntityAsDataSource(Model.SearchParam.Example.Country); %>
<smart:AsyncComboBoxWC ID='Country' runat="'server'" TargetController='Country' ComboBoxShowEmpty='true' /> </div>
</div>
<div class='inputWC inputWC60 w100p'>
<div class='label'><%= GetLocalized(Str.Controllers.Gender)%></div>
<div class='input'><% Gender.SetEntityAsDataSource(Model.SearchParam.Example.Gender).SetComboBoxName(Str.Controllers.Gender) ; %>
<smart:AsyncComboBoxWC ID='Gender' runat="'server'" TargetController='Gender' ComboBoxShowEmpty='true' /> </div>
</div>
</div>
</fieldset>

Country and GenderCountry it is not a good idea to allow the system to delete the Country Germany from the available Countries. This would result in a situation whereby an entity uses an entity which no longer exists in the system. This is similar to foreign key data integrity at database level. We do not reply on the database to take care of this, it is more efficient to handle such situations in the code so we will see how this is done now. 
CountryFacade to ask the ClientFacade if any Clients use the country which we wish to delete.CheckDelete’ method in the CountryFacade must be overridden and a check should be performed before the deletion of a country is allowed to be processed.CheckDelete method should call another method in the ClientFacade to check if the Country is in use by a Client.IClientfacade) should be extended with a method ‘IsCountryInUse’IsCountryInUse method should be implemented in the ClientFacade.CountryFacade and adding the following code:/// <summary>
/// Must check if current entity is not used!
/// if any other entity use this instance we stop deleting and add an error message
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
protected override bool CheckDelete(Country entity)
{
var result = base.CheckDelete(entity);
if (ClientFacade.IsCountryInUse(entity))
{
Messages.AddError(this, Str.Messages.Keys.CannotDeleteItem, Str.Messages.Templates.CannotDelete1
, entity.ToDisplay());
result = false;
}
return result;
}
ClientFacade so we need to add a local member _clientFacade….#region members
IClientFacade _clientFacade;
#endregion members
We also need a property for ClientFacade:#region properties
/// <summary>
/// Allowes to set Agent using login
/// </summary>
public virtual IAgentFacade AgentFacade
{
protected get
{
if (_agentFacade.IsNull())
{
_agentFacade = FacadeFactory.CreateFacade<IAgentFacade>(Messages);
}
return _agentFacade;
}
set
{
Check.Require(value.Is(), " IAgentFacade cannot be null");
_agentFacade = value;
}
}
#endregion properties
CheckDelete method above calls the method IsCountryInUse and will determine whether the Country can be deleted based on the results of that call.IsCountryInUse must be added to IClientFacade:/// <summary>
/// Allows to provide check before delete.
/// Is there any Agent using 'entity' instance as Country
/// </summary>
/// <param name="entity"></param>
/// <returns>true if is in use</returns>
bool IsCountryInUse(Country entity);
Note that it is also necessary to add a using directive so the interface has access to the CodeList namespace because it needs access to the Country object:using Firm.SmartControls.Entity.CodeLists;
The above using directive also needs to be added to the ClientFacade.IsCountryInUse method in the ClientFacade.#region IClientFacade
/// <summary>
/// Provides checking before a deletion takes place.
/// Are there any Clients using 'entity' instance as Country
/// </summary>
/// <param name="entity"></param>
/// <returns>true if is in use</returns>
public virtual bool IsCountryInUse(Country entity)
{
var item = Dao.GetBySearch(new ClientSearch()
{
MaxRowsPerPage = 1,
Example = new Client()
{
Country = entity
}
}).FirstOrDefault();
if (item.Is())
{
Messages.AddWarning(this, Str.Messages.Keys.ItemInUse, Str.Messages.Templates.ItemIsUsedForEntity3
, entity.ToDisplay(), item.ToDisplay(), Str.Controllers.Country);
return true;
}
return false;
}
#endregion IClientFacade


General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 29 Jun 2009 Editor: |
Copyright 2009 by David O'Sullivan Everything else Copyright © CodeProject, 1999-2010 Web20 | Advertise on the Code Project |