March 28, 2012

Advanced state management in Extjs GUI

In this post i'll highlight some advanced usage concepts of the state management features available in the Extjs based user-interface. Please note that this post is written having as reference 1.0-M8 and some of the features described here are available only starting with the 1.0-M8-SNAPSHOT-4 build. For those who are interested however, the source code is already available in the SVN.

I'll use for example the sales order management frame as it already implements several advanced concepts - it is easier for you to see the results and for me to copy-paste the code :)
Also, i assume the reader is already familiar with the basic building blocks of DNet Extjs based user-interface.

This frame is implemented in the net.nan21.dnet.module.sd.order.frame.SalesOrder_UI class. It has several data-controls but we are going to refer to only two of them,  net.nan21.dnet.module.sd.order.dc.SalesOrder which implements the document header and net.nan21.dnet.module.sd.order.dc.SalesOrderItem  which implements the document lines. They are referenced in the frame as order and item. 
Here are some screenshots to identify the elements :


Order edit forms

Order items list

Order item form
And the following are the requirements :


The order can be edited as long as its `Confirmed` flag is `No`. Once an order is confirmed cannot change its items nor its header data except the billing and shipping information. After invoiced, the billing information should be also disabled. If an order is confirmed but not invoiced it can be un-confirmed but after invoicing it not anymore. 
The customer and supplier fields of the order cannot be updated, are enabled only when creating the record.


Let's see how to implement the above requirements:





Due to its highly modular and component oriented architecture the basic user-interface building blocks are also scoped. So we have two data-controls in this frame order and item, but they are not aware one of the other. It should be possible to reuse one of them in a frame without the other one. The higher scope ( the frame) is the context which is aware of both of them. Inter DC (data-control) communication must be managed at frame level.
This is the first important rule to consider when deciding what logic at what level/context (data-control or frame) is implemented. 


The second important rule is at DC  level. A DC may have many predefined views and not all of them used all the time. So the views are not aware of each other. But if a view may or may not be used in a certain frame the controller also is not aware of them. Although it could be made aware of the present views it is not desirable. There are many possible view types (regardless if they are already implemented or will be in future) we should not want to make our controller an encyclopedia to know everything. 
So the controller implements the functional logic / flow and fires events for important tasks. The views subscribe to the events they are interested in and manage their elements accordingly. User interaction on views results in invoking controller actions based on each view's role. 
This is the second important rule to consider what logic where should belong : view level or controller level inside of a DC. 


To make a long story short let's see the code snippets. 


1. The Customer and Supplier fields are not allowed to be changed once an order is created: 
In SalesOrder$EditMain mark these fields with noUpdate:true Similar for noInsert


.addLov({ name:"customer", noUpdate:true, ...


2. If the order is confirmed disable the SalesOrder$EditMain DC form view fields :
Override the _beforeApplyStates_ as below:

,_beforeApplyStates_: function(record) {
if (record.get("confirmed") || record.get("invoiced") || record.get("delivered") ) {
this._disableAllFields_();
return false;
}
}

When a new record is bound to a form it calls the _applyStates_ method decorated with _beforeApplyStates_ and _afterApplyStates_ . As we don't care about the standard rules implemented in the _onApplyStates_ we just apply our rules and ignore the rest.

Leaving the standard _onApplyStates_  to be executed in DC form view, the following happens:

  1. Check if the data-control is set as readOnly and execute the _doDisableWhenDcIsReadOnly_ function if it should be executed. This is controlled by the _shouldDisableWhenDcIsReadOnly_ flag and the execution mode by the _disableModeWhenDcIsReadOnly_ property. Several predefined functions are available which are invoked based on the configured mode. Of course they can be overriden or other modes configured according to specific needs (check the dnet.core.dc.AbstractDcvForm class).
  2. Check the noInsert/noUpdate flags for each field and enable /disable them based on the record state (insert/update)
  3. Check the result of the _canSetEnabled_ : function(fieldName, record) for the field and apply the result. By default this function just verifies if there is an _enableFn_ function defined by the field and if exists , invoke it in the form view scope. The boolean result is the state true-> enable, false -> disable 
  4. Call  _afterApplyStates_  where you can add additional logic

3. If the order is invoiced disable the billing related fields in the SalesOrder$EditDetails form view:
Override the _afterApplyStates_ as shown below

,_afterApplyStates_: function(record) {
if ( record.get("invoiced") ) {
this._disableFields_(["billTo","billToLocation" ]);
}
if ( record.get("delivered") ) {
this._disableFields_(["shipTo","shipToLocation" ]);
}
}



4. Setting an order as confirmed is done with a data-service, the same is true to mark it as invoiced. The `Generate Invoice` calls a server-side data-service which generates the invoice and updates the order as invoiced. While for invoicing the order this approach seems obvious right from the beginning, to set it as confirmed may not be so obvious - why to use a service and not a checkbox which can be checked/unchecked and result persisted by saving the record. The reason is that this way the two server-side service methods can be authorized and the rights to confirm/un-confirm an order as well as invoice it can be granted to certain users by privileges - roles ( see the post about acces-control )


After calling this server-side service method the form should re-evaluate its state :
So we have to register a listener for data-service calls and re-apply the form state. Override the _endDefine_ function in SalesOrder$EditDetails as shown below:

,_endDefine_: function() {
this._controller_.on("afterDoServiceSuccess", function(dc, response, name, options) {
this._applyStates_(dc.record);
 } , this )
}


The _applyStates_ is automatically called in many situations like change current record, record becomes dirty or clean, successful save, etc but in case of service methods call is not called automatically. The reason is that it may be or may not be necessary to re-evaluate state. In this particular case would be necessary, but for example at sales order item level when selecting a product, a similar service method is called to get the product price based on document price list. In this case is not necessary to re-evaluate states.


5. The frame level buttons should be enabled / disabled also based on the confirmed invoiced state
In the SalesOrder_UI the button definition should be:

.addButton({
name:"btnConfirmOrder",text:"Confirm",
handler: this.onBtnConfirmOrder,scope:this,
stateManager:{
name:"selected_one_clean", 
dc:"order" , 
and: function(dc) {
return (dc.record && !dc.record.get("confirmed"));}
}
  ....

The button is enabled when there is only one order selected, there are no outstanding changes at document header level and the current order is not confirmed.


The `Generate Invoice` button is enabled when there is only one order selected, there are no outstanding changes at document header level, the current order is confirmed and not already invoiced.

.addButton({
name:"btnCreateInvoice",text:"Generate Invoice",
handler: this.onBtnCreateInvoice,scope:this,
stateManager:{
name:"selected_one_clean", 
dc:"order" , 
and: function(dc) {
return ( dc.record && dc.record.get("confirmed")
                                && ! dc.record.get("invoiced"));}
}
 .....

6. The order items should be read-only if the document is confirmed; should not be possible to create new lines, modify or delete existing ones. In this case if the document is confirmed we mark the items data-control read-only and let the framework do the rest of the work:

Add the following function in SalesOrder_UI to manage the read-only state for the items data-control :


,_syncReadOnlyStates_: function() {
var orderRec = this._getDc_("order").getRecord();
var itemsDc = this._getDc_("item");
if (orderRec.get("confirmed")) {
if (!itemsDc.isReadOnly()) {
itemsDc.setReadOnly(true);
}
} else {
if (itemsDc.isReadOnly()) {
itemsDc.setReadOnly(false);
}
}

and tell the order DC to execute this function whenever the current record is changed or a server-side service is called ( resolving the transitions for the confirmed / un-confirmed states):


,onAfterDefineDcs: function() {  
this._getDc_("order").on("afterDoServiceSuccess", 
function() {
                     this._applyStateAllButtons_();
                     this._syncReadOnlyStates_(); 
        } , this );

this._getDc_("order").on("recordChange", this._syncReadOnlyStates_, this );
}






No comments:

Post a Comment