Click here to Skip to main content
15,890,123 members
Articles / Web Development / ASP.NET

Web-Application Framework - Catharsis - part XI - Controller layer

Rate me:
Please Sign up or sign in to vote.
4.33/5 (2 votes)
22 Mar 2009CPOL11 min read 33.6K   12  
Catharsis web-app framework - Controller layer in MVC design pattern

MVC - Controller layer

This article is next step in Catharsis documented tutorial. Catharsis is web-application framework gathering best-practices, using ASP.NET MVC 1.0, NHibernate 2.1 . All needed source code you can find on   

http://www.codeplex.com/Catharsis/

Catharsis-title.png


No.Chapter No.ChapterNo.Chapter

I New solution VI CatharsisArchitectureXI Controller layer

II Home page observationVII Entity layer XII UI Web layer 

III Roles and usersVIII Data layer XIII Tracking, PAX design pattern

IV Localization, translationsIX Business layerXIV Catharsis Dependency Injection (DI, IoC)

V Enter into Catharsis, new EntityX Model layer XV (Code Lists - undone)

Controller layer

This story is based on the version 1.0.1 and in comparison with previous (some almost half a year old) it would be correct even in the future versions.

This article is intended as an tutorial overview for Catharsis developers. The aim is to allow you quick start. Examples how-to develop particular projects will follow...

Controller layer strongly uses the ASP.NET MVC framework and provides two main base classes WebController and its child EntityController. Both objects are nested in the ProjectBase.Mvc project.

WebController  

If you will use the Catharsis Guidance, you can be sure that your controllers are always derived from WebController (entities use the child EntityController described below)
If you'd like to extend the base functionality, I suggest to implement this into your Company.Product.Controllers project base classes called BaseWebController and BaseEntityControllerThe WebController encapsulates two fundamental features of the Catharsis framework. (Messages and Model binding) 

IList<ApplicationMessage> Messages 

WebController implements the ICoreController interface. It declares one property Model of type ICoreModel, which has the collection IList<ApplicationMessage>. Any descendent controllers therefore can expect that the property Model contains Model.Messages. Messages are so important that they will be described in more detail in the future - for now: You can fill it with any Information, Warning and/or Error (including Exceptions). Every Message gathered in this collection during one request is finally displayed to user and logged (if allowed in log4net.config). If there is any  Error found, you can be sure that no change will be persisted - transaction roll-back is called. 

Model binding 

The second and I guess very frequently used feature is the "Model binding" functionality. On any controller you can really easily fill your IModel object with sent data. There is one simple method call available, which you can use for converting the Form or/and QueryString data into properties of provided model.   

C#
public virtual bool BindModel(object model); // declaration
C#
BindModel(MySpecialModel); // day to day usage

As you can see, BindModel() operation will fill any object (not only IModel)
Every ‘Key’ in the pair collections Form and QueryString is evaluated: does the name fit to any property of the model instance?
There is one restriction: property type must be of the “System” namespace (decimal, int, string, DateTime etc.).  Any other object must be bounded manually in your derived controllers (see below)

File Binding   

Method BindModel() will also fill the byte[] properties if there was an byte array uploaded:  

XML
<form action='Entity.mvc/Action' method='post' 
    id='mainForm' enctype='multipart/form-data'>
  <input type='file' name='MyFile'>
</form>

will fill the property:

C#
public virtual byte[] MyFile {get; set; }

On UI WebControls always use the CreateForm() method to generate the FORM starting tag (end tag is generated automatically after ButtonSubmit). This method not only uses data from the IMasterModel (to correctly set action), it also appends the essential attribute enctype='multipart/form-data' which allows the files uploading.

Binding + Messages 

There could be incorrectness in the sent data, misspelling or misunderstanding. The offence against the business law should be checked on the IEntityFacades. The type misspellings on the other hand must be handled on the controller layer – during the Model binding.
The BindModel() will do that job using the Model.Messages. For example when binder does not succeed to convert string into a DateTime property: new Error Message is inserted into Model.Messages collection.

The binding process (iteration through all the 'keys' in the Form and QueryString) is not interrupted, when the first Error appears! Other words: every 'key' is evaluated and if needed – a Message about troubles is appended.

This approach has an impact on user’s comfort. What could be bounded – is bounded. It means that when the user is returned back to the editing page – all correctly filled properties are stored and displayed (no need to retype them) only missing or misspelled are asked to be corrected. User gets the information about all found misspellings at once.

The Catharsis framework also evaluates the Model.Messages collection on many places. If there is at least one Error, transactions are rolled-back, and user’s are redirected to editing screens. And also provided with localized information about troubles (another strong ability of the Messages feature).  

WebController summary 

Keep in mind:  

  • Insert any Message in the controller.Model.Messages collection and user will be informed about it.
  • If Logging is allowed, every Message is logged (see Log4Net.config)
  • Insert Message with severity ‘Error’ and current transaction will be rolled-back.

Every Controller in the application should be (must be) derived from WebController. The reason is strong reuse of once implemented stuff

EntityController   

Entity Controller regionsCatharsis OOP architecture provides powerful entity handling. 

The EntityFacades and EntityDaos mostly provide only rough CRUD (create, read, update, delete) operations. In comparison the EntityController provides more detailed CRUD handling – to be kind and user friendly. Take a look at this picture:

These regions should give simple, clear idea about implemented features, nested in this base controller.

Every entity can be searched, listed, added, updated and deleted. Users can use paging on the list or detail level, export selected entities into the MS Excel xml 2003. Every action fills the known properties exposed by IMasterModel e.g. fills actions, buttons, lists, bread-crumb control path…

CRUD Actions 

Add(), Delete() … mostly won’t be in the center of your interest. You’ll simply use them without any need to overriding or hiding them (but you can).
More interesting is the design of these actions. Some of them need to be slightly adjusted to fit specific needs of the target entity.

For that purposes there are two types of special operations: OnBefore and OnAfter operations

OnBefore operations 

Some actions calls the OnBefore operations (see the list).  

C#
#region protected OnBefore Actions

protected virtual void OnBeforeSearch()
protected virtual void OnBeforeSearchingResults(int? id)
protected virtual void OnBeforeList()
protected virtual void OnBeforeListToExcel()
protected virtual void OnBeforeListResults(int? id)
protected virtual void OnBeforeDetail(int id)
protected virtual void OnBeforeEdit(int id)
protected virtual void OnAnyActionWhenBeforeIsFinished() { }

#endregion protected OnBefore Actions 

This (OnBefore operation) is usually the best place for you to override and extend or hide some new features.
Let us take a closer look on the on BeforeSearchAction:  


C#
protected virtual void OnBeforeSearch()
{
    Model.ItemModel.DoNotStoreItemInSessionStorage = true;
    Model.SearchParam.CurrentPage = 0;

    Model.MasterModel.FormUrl = Url.Action(Constants.Actions.EntityDefault, ControllerName);
    Model.MasterModel.CurrentAction = Constants.Actions.Common.Search;

    Model.ContentCssClass = " searchContent ";

    this.AddAction(Constants.Actions.Common.New, Url)      // sets the basic Actions
        .AddAction(Constants.Actions.Common.Search, Url)
        .AddAction(Constants.Actions.Common.List, Url);

    Model.MasterModel.ButtonSubmit = new ButtonItem(Constants.Actions.Common.Search);
    this.AddButton(Constants.Actions.Cancel.Clear, Url);
    OnAnyActionWhenBeforeIsFinished();
}

As you can see the main goal of this operation is the Model filling. The DoNotStoreItemInSessionStorage switches off the Model.Item to be kept in the ISessionStorage (which can be handy when editing compounded entity – edited item is stored in the session while searching for other entities, CodeLists etc. – before fulfills all business rules and can be stored in the database).

CurrentPage number is reset to start at the beginning. This is a good (may be necessary) habit, to reset the paging – current page could be higher than the page count after applying new filter.

FormUrl provides crucial information for the form gathering searched information. 

CurrentAction can be used for example in the bread-crumb control to inform user.

ContentCssClass allows decorating the main area with different style – it is nice to know that gray background is detail, and the orange is search screen – it is significantly less confusing then common white background. Actions which are available on the searching screen are appended.
ButtonSubmit - the submit button object (There is only one submit button for the main area <FORM> – other buttons has its own <FORM> element. This new feature of the ASP.NET MVC is in fact very powerful. The form which is submitted sends only its data <input> and that is very effective.) 

Finally the OnAnyActionWhenBeforeIsFinished() is called. It could be handy in situations that you want to extend some Model property for every action.

Other OnBefore actions work very similarly.  (You should take a look)

OnAfter operations 

There are four OnAfter operations

C#
#region protected onAfter Actions

protected virtual bool OnAfterBindSearch()
protected virtual bool OnAfterBindModel() 

protected virtual void OnAfterStoreModel(ActionExecutedContext filterContext, IDictionary<string, object> otherElements)
protected virtual void OnAfterRestoreModel(ActionExecutingContext filterContext, IDictionary<string, object> otherElements)

#endregion protected onAfter Actions

The OnAfterBindModel and OnAfterBindSearch will be probably in permanent use. These operations are the best place to append manual Model binding for properties which are not of the type from the “System” namespace.
Take a look on a search screen for Users.

SearchUser2.png

 

User can select one role from the combo box to filter the results. To append this filter to the search criteria we need to implement the AppUser’s controller OnAfterBindSearch operation this way: 

C#
protected override bool OnAfterBindSearch()
{
    var result base.OnAfterBindSearch(); // do not forget to call base
    int id;
    if (Request.Form.AllKeys.Contains(Str.Controllers.AppRole))
    {
        if (int.TryParse(Request.Form[Str.Controllers.AppRole], out id))
        {
            Model.SearchParam.AppRole = FacadeFactory.CreateFacade<IAppRoleFacade>(Model.Messages).GetById(id);
        }
        else // There is a key with no value == user selected "---"
        {
            Model.SearchParam.AppRole = null;
        }
    }
    return result;
}

Do not forget to call the base.OnAfterBindSearch() - the mother class automatically (always) calls to bind the Model.SearchParam - any string, int, DateTime etc. property is therefore bounded this way for you.

OnAfterBindModel accordingly will be the place for binding your Model.Item when New() or Edit() action is processed. 

OnAfterStoreModel and OnAfterRestoreModel

The Catharsis framework uses the approach: New objects per request. It simply means that every user’s action is handled by newly created controller and newly created model. The impact is that nothing can be stored in the models between two requests.
For objects which should be kept among many requests there is the ISessionStorage (currently uses the ASP.NET Session, but it could be Cache cooperating with DB, or anything else you need).

Any object stored in the ISessionStorage must be space-efficient (simply of a small size). Therefore Catharsis stores only Model.SearchParam containing the filter criteria and Model.Item if the entity is edited and cannot be stored yet. Everything else is recreated (usually from DB or some Cache or static elements) for every request.

If you need to store something between some actions – simply override the operations OnAfterStoreModel and/or OnAfterRestoreModel. The provided 

C#
object IDictionary<string, object> otherElements

is the right place to store your object. But DO NOT forget to remove this object when is not needed!

Note: Keep in mind that user can do unexpected steps. If you expect Action1, Action2, Action3 as a chain, you can store something in ISessionStorage during Action1 and wish to remove it in Action3. But user can change this process and navigate to different action or controller. Therefore you should have some smart ISessionStorage cleaner...

Ajax action GetComboWC 

ASP.NET MVC itself strongly, and natively supports AJAX Controls. The reason is in the framework (ASP.NET MVC) architecture, where controller actions can result in Views or Controls. The only difference is in the generated html (Page with HTML BODY ... and Control with only needed <div>content</div>)

Catharsis uses async mostly for comboboxes (but you do not have to!). Why render the Page with <Select> if user may not need it. Better is to provide small button, which after click downloads the dropdown. Until button is clicked Catharsis renders the selected value as hyperlink (proven as user-friendly).

How to create the AJAX combo? 

Almost  any EntityController could provide its entities via the ComboBox. The only limitation is the amount of contained values - 20 entities in the combo is OK but 200 is horrible design.  

We’ve already met this on the User search screen – there was the list of the available application roles. How it works?

AppRole controller has the Action GetComboBoxWC() 

C#
public virtual ActionResult GetComboBoxWC()
{
    var model = ModelFactory.CreateModel<IComboBoxModel>();
    model.ComboBoxName = ControllerName;

    string currentID = null;        // was there any previous item
    model.ShowEmpty = true;
    if (Request.Params.AllKeys.Contains(Str.WebControls.Ajax.ParamCurrentID))
    {
        currentID = Request.Params[Str.WebControls.Ajax.ParamCurrentID];
    }
    if (Request.Params.AllKeys.Contains(Str.WebControls.Ajax.ParamShowEmpty)
     && Request.Params[Str.WebControls.Ajax.ParamShowEmpty].ToLower() == "false")
    {
        model.ShowEmpty = false;
    }

    model.Items = new SelectList(Facade.GetAllRoles() // this will vary on other controllers
        , Str.Common.ID
        , Str.Business.AppRole.RoleName // this is the displayed value
        , currentID);

    if (Request.Params.AllKeys.Contains(Str.WebControls.Ajax.ParamCssClassName))
    {
        model.CssClass = Request.Params[Str.WebControls.Ajax.ParamCssClassName];
    }

    return View(Str.WebControls.ComboBoxWC, model);
}

The essence of every Action is the IModel filling. As you can see, all the processing has one aim: gather data (Form, QueryString, IFacade) and fill the model which is passed to smart View - to render ComboBoxWC. Separation of concern in its base form.

You can use the copy-paste to extend your other controller with this Action. The changes will go mainly the new SelectList() statement - dependent on your entity IFacade.

Next step is to put small piece of code into the EntityDetailWC.ascx:

HTML
<div class="inputWC inputWC70 w100p">
  <div class="label"><%= GetLocalized(Str.Controllers.AppRole)%></div>
  <div class="input" <%= TagId("div" + Str.Controllers.AppRole) %>> <%= this.TextOrComboBox(Str.Controllers.AppRole 
    , Str.WebControls.Ajax.ActionGetComboBoxWC, Model.SearchParam.AppRole, "div" + Str.Controllers.AppRole)%></div>                  
</div> 

button.png(InputWC70 will grant 70% of space to the second div class="input" )

This will result in the ComboBox (hyperlink and button till clicked)
displayed on a picture above.  

The last part is to Bound this information
- again as described above (OnAfterBindSearch())

OnList operations 

The List() action would be one of that you will never override (but still: you can). Displaying the list is in fact very straightforward: Just to call IEntityFacade with Model.SearchParam and get the IList<Entity>.
The only and essential difference is in the displayed columns. And that is the job of the OnListToDisplay operation.  

DO NOT call Base.OnListToDisplay() - OnList actions, as opposite to others, should fully hide the base implementation.

The only purpose of this method is to fill the Model.ListModel.ItemsToDisplay collection 

C#
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 = Str.Business.AppUser.Login, Value = i.Login} ,
                new HeaderDescription { HeaderName = Str.Business.AppUser.SecondName , Value = i.SecondName },
                new HeaderDescription { HeaderName = Str.Business.AppUser.FirstName , Value = i.FirstName } ,                        
                new HeaderDescription { HeaderName = Str.Common.ID , Value = i.ID.ToString(), Align = Align.right },
                // append or adjust new columns using HeaderDescription 
            }
        } as IItemToDisplay);
}

The basic skeleton of this operation is generated by Guidance. And as the implementation on  AppUser controller shows,  your goal is to append as many new HeaderDescriptions as needed.
Than the ListWC.ascx WebControl comes into play and uses this information to render the localized, orderable list, with navigation, edit and delete buttons, displaying checkboxes for bool values etc.  

That all is possible because of this rich object

C#
new HeaderDescription()
{
    HeaderName      = "The key which will be localized",
    HeaderNameArea  = " Area for more precious localizing ",
    IsBoolean = true,    // will display checkbox
    Align = Align.right, // for numbers
    Localize = true,     // false will render the HeaderName instead of localizng 
    MaxCellLength = 20,  // the whole Value will be used for title, cutted value in the <td></td>
    Sort = true,         // false to switch the sorting off
    SortByObject   = "Boss.Address", // If the current entity has 
    // property "Boss" of type 'Person' which has the property  "Address" of type 'Address'
    SortByProperty = "City", // of type 'string'
    Value          = "string which will be displayed in the cell "
}
OnListToExcel is very similar. It provides the columns which should be exported into the MS Excel xml 2003 file.

Controller summary   

MVC design pattern is easy to use and understand, allowing and supporting powerful architecture. I believe that this story will help you to use it - in a full power with Catharsis. 

Enjoy Catharsis
Radim Köhler

Sources

All needed source code you can find on  

http://www.codeplex.com/Catharsis/

How do I see the MVC ver. MVP:http://www.codeproject.com/KB/aspnet/AspNetMvcMvp.aspx 

History      

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Czech Republic Czech Republic
Web developer since 2002, .NET since 2005

Competition is as important as Cooperation.

___

Comments and Discussions

 
-- There are no messages in this forum --