Apr 15, 2008 / Should We Have Parallel Releases and Iterations in a Project?

The story began few months ago when we restricted creation of overlapping releases in TargetProcess. What does it means? People lost a possibility to create several parallel releases. The answer is pretty simple if you have several teams working on the same large project. Definitely you may have parallel releases and in fact each team may have own release schedule which should be approved/synchronized with other teams, but it may have different end date. It is not a mandatory to have all releases from all teams in the same day X.

But what if you have one team and one project? The main concern is something like that: “We are working on version 1.5 now, but we need to releases some patches for version 1.0 and also some people from our team working on version 2”. Sounds like a natural situation. Indeed we have to release some patches with bug fixed as soon as possible since it affects current customers and they need resolutions right now. Indeed we are developing new version with defined set of functionality. And hey some our gurus started prototyping several killer features that are not will be a part of version 1.5, but may be released in version 2. All it may look reasonable. Isn’t it?

Well, the simple answer is no, it isn’t. Let’s dig into details. For example, we have a team with 6 developers (skip QA and other team members for simplicity). You have 1 month releases schedule. You have defined scope for release 1.5 and some features for release 2. You know that you should also release 2 or 3 patches during this month for version 1, but the scope of the patches is undefined at the moment. How will you plan the release(s)? “OK, Joe, you will work on patches when some important issues will be reported by customers. But till that time you may work on Feature X for release 1.5. Mark, Beth and Teddy will take all other features from release 1.5. Also we have some very important and complex features that planned for version 2. We should mitigate risks and prove the concepts right now. Tom and Jerry will work on that”. What we have in the end?

Joe belongs to both releases. Other people do not have multi-tasking. Everything looks good. In reality we may end up with some bad things:

  • Some issues assigned to release 1.0.x are hard to fix and Mark will help Joe to fix them.
  • Joe spent too much time on bug fixes and unable to complete Feature X on time. So it will be required to exclude the feature from release 1.5 (or call someone from release 2 to help Joe).
  • Beth’s feature appeared to be more complex than expected. Again two options (drop it or call for help).
  • Jerry did not finish his work for release 2, since he was helped Beth and Joe in release 1.5
  • Tom did all fine, but in the end it appeared that customer decided to drop the feature from the release 2 at all and replace it with another feature.

I agree that all above sounds pessimistic. But didn’t you have similar problems in the past?

Now we have 3 people with multi-tasking and all Tom’s work out of context (he did not add any value to the releases). Obviously, our plan failed. Why? We violated some core principles of agile development:

  • Make decisions as late as possible. In fact Tom and Jerry’s assignment to release 2 was a mistake, since the release 2 will happen next to release 1.5. Important addition: It is OK (and even recommended) to proof all possible concepts before project start or in release 1.
  • Reduce Work in Progress (WIP). The more WIP you have for given resources, the later you will release something. If we have 10 user stories and all of them will be in progress, we will have waterfall process in the end. We will have 90%-done problem and all related risks.
  • Avoid Multi-tasking. Something related to point 2, but with such plan we really increased multi-tasking. Depending of context it may be better to assign Joe full time to Release 1.0.x (if you are struggling with de-motivation problem, you may just rotate developers for patch releases for example).
  • Avoid 100% people loading. You may want to load all people 100%, but that’s a mistake. People should have some free/spare time. It is a proven fact that 100% load decreases productivity/throughput. So if you assign Joe to release 1.0.x and he will do nothing 1-2 days that is not a problem. He may do some minor refactoring or some minor bug fixes. But he will be available just in time when first issue request will be received.

Is this plan better?

Well, maybe. We eliminated all work from Release 2, which is good. Could we eliminate work from release 1.0.x? Probably, if fixes are not very important and may wait till release 1.5 availability. And often they can! Most likely you will have several fixes that MUST be released ASAP. It may take 2-3 days from one of the developer to handle them.

Turning back to TargetProcess ask me a question “Will you make it possible to have parallel releases in a single project?” I am answering “Yes, we will”. In reality things are more complex. You have pressure from top management who knows nothing about lean and agile and you gave up explaining the reason behind one release at a time and 100% people load problem. You have a large team inside one project and want to separate work with releases (why not?). You have maintenance releases and want to track them separately (why not?). Maybe there are more cases. Real life is complex and teams are learning doing agile.

Labels: , , ,

Apr 1, 2008 / TargetProcess Announced Integration with Microsoft Surface

It looks like a magic, but soon it will be possible to manage projects using TargetProcess on a very cool Microsoft Surface table. Releases and iterations planning using drag and drop with usual human hands and fingers (throw your mouse and seal up your touchpad), nice sorting and grouping, fantastic user interface and experience!

"Microsoft Surface brings real tactile feeling into software world. You may operate with entities using hands on a table, almost like with usual cards. You may take a bunch of user stories and throw them to release. You may drag the slider to move release date. You may give a flick on user story to delete it! It really sounds like a miracle. I think such experience is a huge step forward for whole software engineering industry.", commented Michael Dubakov, founder of TargetProcess, Inc.

We are creating new appealing user interface with really handy functionality and unsurpassed usability. Stay tuned and find additional details here!

Mar 18, 2008 / We Have Released TargetProcess v.2.8

Enjoy Customizability: Plugins, Bugzilla Integration and Custom Fields per Process in TargetProcess v.2.8.

Mar 7, 2008 / TargetProcess On-Demand Won Second Jolt Productivity Award

TargetProcess won second Jolt Productivity Award, now in Project Management Tools category!

Jan 9, 2008 / Should We Measure Individual Estimates Accuracy?

Individual estimates accuracy is another metric requested by several TargetProcess users. First of all it may be useful if and only if:

  • Task estimated by one developer only (if whole team estimates tasks you can’t measure individual accuracy, only whole team accuracy).
  • You are using time tracking to track all spent time for all tasks

For example, Ted subscribed to Quick Search user story. He broke down this user story to tasks, estimated each task and got 50 hrs of effort for user story. Then he implemented the user story and spent 60 hrs. It means his estimate accuracy for this task is 50 / 60 = 0.83 or 83%. In the end of iteration we can calculate all Ted’s assignments and spent time and define estimate accuracy for next iteration, let’s say it is 0.7.

OK, but how we can use this metric? Well, if Ted subscribed to 60 hrs work it means he will spend 85 hours and for 2 weeks iteration it means at least 5 hrs overtime. Ted should take this information into consideration and remove some tasks from his ToDo. This works if Ted estimate accuracy remains the same, but is that always the case? In reality Ted’s accuracy may vary from 0.5 to 0.9 and exactly for next iteration it may be 0.9 and in this case Ted will be able to do all committed work.

As you see estimate accuracy coefficient should be used as a recommendation only, but if Ted insists on his commitment Project manager/Scrum master should agree and let Ted do the job. Again we have similar problem as with individual velocity when Scrum master may demotivate developers with numbers in hands.

I see value in individual estimates accuracy, but I see some danger as well. This metric should be used by developers, not by Project managers/Scrum masters.

I think we should care about team estimates and team velocity, not about individual estimates and individual velocity. Agile development is all about jelled teams.

Labels: , ,

Dec 26, 2007 / Should You Measure Individual Velocity?

We are receiving more and more requests to add individual velocity measurement in TargetProcess. People want to see this information and use for iterations planning. But is it required to measure individual velocity? What caveats such calculation has? It may be not clear initially, but there are some problems with individual velocity for sure.

What is an individual velocity? It is a measure of how much effort a person may complete in a defined time frame. In agile development time frame is an iteration. If Ted completed several tasks with 40 hrs of effort in total and Jerry completed tasks with 25 hrs effort only, we should say that Ted has better velocity in last iteration. Does it mean that Ted is a better/faster developer? Not at all. There are hundreds of reasons why Jerry completed less. He helped other developers with tasks and mentored them; he got serious headache during two days in a row and did almost zero progress; he had two birthday parties last week and his performance was no good next day after each party; one of his tasks was underestimated due to unexpected performance problem with third-party component. We can bring many more reasons on the table. Performance in last iteration says nothing.

OK, but what about average velocity? Surprisingly, Jerry’s average velocity is 54 hrs per iteration. Gosh! What happened with Jerry last two weeks? Will his average velocity help us to make correct iteration plan? If we sum up individual velocities all developers will it help us to create a better iteration plan? No, since we already have Iteration Velocity metric and it will be exactly the same. Why should we care about individual velocity in this case? To make better assignments for each person? But we still don’t know how much effort Jerry will complete next iteration. The good guess is from 25 hrs to 60 hrs. Is it helpful? Maybe, a bit. Is it worth the darkside of the individual velocity measurement? And sure there are some problems with it.

Individual velocity measurement has wrong focus on individual performance. The focus should be on team performance, individual performance is not important. If Jerry knows that his velocity is measuring, he maybe will not help other team members a lot. He will focus on his performance as an individual developer. The worst thing company can do is to bind bonuses to individual performance. This nips teams in the bud. The right solution is to measure team performance, but in agile development we already have team performance metric, it is a well-known iteration velocity - very good, very simple and very helpful metric that enough for iteration planning. Individual velocity will not bring additional benefits there.

Individual velocity measurement forces work assignment, while in agile teams it is all about work commitments. Team should do a commitment to complete as many user stories as they feel can be completed during next iteration. On iteration planning meeting Jerry may feel bad about his poor performance in last iteration (yes, developers know their performance without measurements) and will commit to 70 hrs work items in total and complete them all with courage. If you measure individual velocity you tend to assign stories based on numbers you have in hands. “Hey, Jerry, are you kidding? You did only 25 hrs last iteration, how are you going to do 70 hrs?” It is good if Jerry has courage to answer something like “Just believe me, I will do that” and project manager/Scrum master agrees and allows Jerry to make such commitment. But Jerry might think “oh, he doesn’t believe me… Well, let it be, I will take 50 hrs, it is close to my average velocity”. Individual velocity may demotivate people, and many managers having it in hands will use it incorrectly. It is very common to revert back to muscle memory of waterfall days and make assignments instead of commitments (especially when your project is in troubles).

I don’t see much value in individual velocity, but I do see many problems it may cause (or hide, or support).

Labels: , , ,

Nov 30, 2007 / Failure Rates In Early Stage Venture Deals

No, we are not seeking for VC, but still it is interesting to know success/failure rates for Venture Capital.

read more | digg story

Nov 27, 2007 / A Kanban System for Software Development

How to use Kanban to improve SCRUM process

read more | digg story

Nov 2, 2007 / Grid Inline Editing. Part II. Architecture and Customization

In Part I of the article I showed how to use IEC (Inline Edit Controller) to enable inline editing in grid. We've used this inline editing in real project - TargetProcess - ASP.NET based agile project management software. This part describes architecture and advanced features like validation and extension.

Basic Classes

Inline editing solution consists of three classes:
  • InlineEditController - controller class that handles inline editing;
  • EditAreaSettings - class that stores information about what should be processed in IEC;
  • TransactObject - helper object that manages saving procedure (it acts as a synchronizer and restore old values in case of server side error as well as confirm correct saving).
var editAreasSettings = new Array();
var shipNameEditAreaSettings = new EditAreaSettings("ShipName","ShipNameIdPrefix",null, true);
editAreasSettings.push(shipNameEditAreaSettings);
var shipAddressAreaSettings = new EditAreaSettings("ShipAddress","ShipAddressIdPrefix");
editAreasSettings.push(shipAddressAreaSettings);
var shipRegionAreaSettings = new EditAreaSettings("ShipRegion","ShipRegionIdPrefix");
editAreasSettings.push(shipRegionAreaSettings);
var inlineEditController = new InlineEditController('gridID', editAreasSettings, onSaveEventHandler, true);

function onSaveEventHandler(retObj)
{
  var transactionObj = new TransactionObject(retObj.itemID, inlineEditController);
  transactionObj.onStatusSuccess =  onRequestComplete;
  transactionObj.onStatusError =  onRequestError;
  retObj.OrderID  = retObj.itemID;
  TargetProcess.DaoTraining.BusinessLogicLayer.OrderService.UpdateOrder(retObj,
                                     transactionObj.onSaveSuccess ,
                                     transactionObj.onSaveError)
}

Let's take a look at all classes.

InlineEditController

Controller class that handles inline editing.

 var inlineEditControllerObj = new InlineEditController(gridID, editAreasSettings , onSaveEventHandler, isExplicitSave)

Arguments

gridID
Id of the grid

editAreasSettings
Array of EditAreaSettings objects

isExplicitSave
Flag specifying whether a row in edit state should be saved when another row set into edit state. For example, when user clicks on another row, the first row will be saved if this flag set to true

OnSaveEventHandler
Callback function that is called on save event.

IEC does not know how to save the item, so the saving procedure should be provided(for example, it may be Web Service call).
       function onSaveEventHandler(retObj){

           retObj.OrderID  = retObj.itemID;
           MyWebService.OrderService.UpdateItem(retOb);

     }
     

retObj
Object that contains edited values.

Public methods

inlineEditControllerObj.commitChanges(itemID)
This method should be called to confirm that data was successfully saved and there is no need to restore initial values.

inlineEditControllerObj.abandonChanges(itemID)
This method should be called to restore the initial values in case of server side error.

EditAreaSettings

Class that stores information about what should be processed in IEC. The IEC should have information about what and how should be updated. The EditAreaSettings object holds all essential information to edit the item.
 var editAreaSettingsObj  = new EditAreaSettings(areaName,
                        areaPrefix,
                        areaDataSourceControlID,
                        isFocus,
                        onSelectValue,
                        onExtractEditedValue,
                        onRenderEditedValue,
                        onCancelEditValue,
                        onEditValue)

Arguments

areaName
Maps the edited values into retObj (as you remember retObj passed into OnSaveEventHandler and stores all new values that should be saved).

areaPrefix
We find the Edit Area using this argument. The full id of Edit Area is "areaPrefix"+"id of item".

    <asp:TemplateField HeaderText="Ship Name">
              <ItemTemplate>

                    <span id="ShipNameIdPrefix<%# Eval("OrderID")%>">
                       <asp:Label ID="Label1" runat="server" Text='<%#Eval("ShipName")%>'></asp:Label>
                    </span>
              </ItemTemplate>
    </asp:TemplateField>
   
        var shipNameEditAreaSettings = new EditAreaSettings("ShipName","ShipNameIdPrefix",null, true);
        editAreasSettings.push(shipNameEditAreaSettings); 
        var inlineEditController = new InlineEditController('uxGridID', editAreasSettings, onSaveEventHandler, true);
...

areaDataSourceControlID
Id of a dataSource control

When edit state enabled for row, all labels transferred into editable state. It means that text will be replaced by input field. Another scenario is when text value should be replaced by Select Box with predefined values to select. IEC will try to set value in the select box based on text in column. DataSourceControl is a drop down contains all required values.

isFocus
Should Edit Area receive a focus when edit state becomes active?

onSelectValue
In most cases, you will use this handler to set correct value in DataSource Control.

onExtractEditedValue
A custom handler may be used to extract edited values from Edit Areas.

onCancelEditValue
A custom handler may be used to set edit area into original (read-only) state.

onEditValue
A custom handler may be used to set edit area into cool (inline edit) state.

OnRenderEditedValue
A custom handler may be used to render original (read-only) with some specialties, like display an image that represents Priority or something like that.

All these handlers can be omitted. You should use them for complex cases.

TransactObject

Helper object that manages saving procedure (it acts as a synchronizer and restore old values in case of server side error as well as confirm valid saving).
   var transactionObject  =  new TransactionObject(itemID, inlineEditController);

Arguments

itemID
ID of an item that will be updated.

inlineEditController
The reference to the IEC.

IEC LifeCycle

IEC life-cycle is not a simple thing I should say. It is quite hard to describe how it works in short, but let's try.

Stage 1. IEC Initialization

IEC binds event handlers to corresponding elements through the whole grid. We have just three handlers: Edit, Save and Cancel. So bind them to required controls.
function InlineEditController(gridID, editAreasSettings , onSaveEventHandler, isExplicitSave)
 { 
   ...
   //******************** Fields ****************************
   var activeInlineEditPanel;
   var thisRefInlineEditController = this;
   //the object that stores the previous values of item, used in case of calling abandonChanges(itemId) 
   var previousStates  = new Object();
   attachEvents();
   function attachEvents()
   {   
       var grid = document.getElementById(gridID);
       if(grid == null)
       {
           alert("error: Unable to find a grid.");
           return;
       }
       //assign the click handler to the grid(HTMLTable)
       grid.onclick = onGridClick;
       
       for(var index=0; index < grid.rows.length;index++)
       { 
         var cells = grid.rows[index].cells;
         for(var index1=0; index1 < cells.length; index1++) 
         {
              var element = findInlineEditPanelInElement(cells[index1]);
              if(element == null)
                 continue;
              if(element.attributes == null)
                 continue;
              //look up for "Inline Edit Panel"   
              if(element.attributes[INLINE_PANEL_FLAG_ATTRIBUTE] != null)
              {  
                 var row = findRowForElement(element);
                 row.ondblclick = onDblClickRow; 
                 row.onkeypress = onEditing;
                 var children =  extractDHTMLElements(element);
                 for(var index2 = 0; index2 < children.length; index2++)
                 {    
                      //look up for save, edit and cancel image buttons
                      var control = children[index2];
                      switch(control.attributes[INLINE_ACTION_ATTRIBUTE].nodeValue)
                      {
                         case INLINE_EDIT_ACTION: 
                           control.onclick = onEditAction;
                         break;
                         case INLINE_SAVE_ACTION:
                           control.onclick = onSaveAction; 
            ...

Stage 2. Enabling Edit Mode

What happens when user clicks Edit link? Maybe the system crashes as usual? Not at this time! It sets table row into editable state. The most interesting method is editRow (hmm, it looks like it should be refactored a little, ok, I will leave you some space for improvements :). So what it does.
  1. Hide some actions and show inline editing actions instead.
  2. Highlight editable row with fancy green color (well, may be customized if you prefer pink).
  3. Store current values (we may need them later if user pushes cancel or some server problems arise).
  4. Call specific onEditValue handler if exists.
  5. Create input element to allow user actually edit something.
//********************* Events ************************ 
    ......
   //fires on edit image click 
   function onEditAction()
   {   //suppose that some row can be in Edit State 
       cancelEditRow();
       editRow(this.parentNode);
   }
   
   //fires on double click on row
   function onDblClickRow(eventObj)
   { 
      if(eventObj == null)
         eventObj = event;
      var srcElement = eventObj.srcElement ? eventObj.srcElement : eventObj.target;
      if(srcElement.tagName == "A" )
        return;
      //look up for "inline edit panel" 
      var inlineEditPanel = findInlineEditPanelInElement(this);
      if(inlineEditPanel)
      {
         cancelEditRow();
         editRow(inlineEditPanel);
      }
        
   }
   
   
   function editRow(inlineEditPanel)
   {   
       if(activeInlineEditPanel != null)
       {
            alert("error: Unable to set edit state. There must be no item in edit state.");
            return;
       }
         
       activeInlineEditPanel = inlineEditPanel;
       var children = extractDHTMLElements(activeInlineEditPanel);
       for(var index=0; index < children.length; index++)
       {  
          var control = children[index];
          switch(control.attributes[INLINE_ACTION_ATTRIBUTE].nodeValue)
          {
             case INLINE_EDIT_ACTION: control.style.display='none'; break;
             case INLINE_SAVE_ACTION: control.style.display='';break;
             case INLINE_CANCEL_ACTION: control.style.display='';break;
             default:/* "Do nothing" */; 
          }
       }
       
       highLightInlineEditRow(INLINE_ROW_BACKGROUNDCOLOR_ACTIVE);
       var itemID = getActiveInlineEditPanelID();
       if(itemID == null)
       { 
           alert("error: Unable to find item id.");
           return;  
       }
       // create a new object to store previuos values (just in the case)
       previousStates[itemID] = new Object();
       for(var index=0; index < editAreasSettings.length; index++)   
       { 
          var editAreaSettings = editAreasSettings[index]; 
          
    //get the Edit Area ID 
          var editAreaID = editAreaSettings.areaPrefix+itemID
          var editArea = document.getElementById(editAreaID);
          if(editArea == null)
             continue;
    
          //saving particular value
          previousStates[itemID][editAreaID] = editArea.parentNode.innerHTML;
          //check if there is specific handler on edit event.  
          if(editAreaSettings.onEditValue != null)
          {   
               editAreaSettings.onEditValue(editAreaSettings, editArea);
          }
          //otherwise call predefined handler of IEC 
          else
          {
             if(editAreaSettings.areaDataSourceControlID == null)
                 editTextField(editArea);
             else
                 editDataSourceField(editArea, editAreaSettings.areaDataSourceControlID);
          }
          //check if there is specific handler on select event.  
          if(editAreaSettings.onSelectValue != null)
             editAreaSettings.onSelectValue(editAreaSettings, editArea);
          else
             //otherwise call predifined handler of IEC 
             selectValueInDataSourceControl(editArea);

           if(editAreaSettings.isFocus)
             setFocusForEditableField(editArea);
       }
       
   } 
   
   

 
There are two possible customizations at this stage:

onEditValue
A custom handler may be used to set edit area into cool (inline edit) state. Example

onSelectValue
A custom handler may be used to set correct value in DataSourceControl. Example

Stage 3. Save Changes

First we should extract new values from form fields. The methods with very original names extractEditedValues and extractEditedValue do all the job. As a result we have an object with all new values. Then we call onSaveEventHandler to save new values into database.
 
function onSaveAction()
   {    
       var obj = extractEditedValues();
       //call the saving handler(the procedure that is defined outside of IEC)
       if(onSaveEventHandler != null)
       onSaveEventHandler(obj);
       //get row back into read state 
       cancelEditRow();
   }
   
      
   function extractEditedValues()
   {    
       if(activeInlineEditPanel == null)
          return null;
       var retObj = new Object();
       var itemID = getActiveInlineEditPanelID();
       if(itemID == null)
       { 
           alert("error: Unable to find item id.");
           return;  
       }
       retObj.itemID = itemID; 
       for(var index=0; index < editAreasSettings.length; index++)   
       {    
            var editAreaSettings = editAreasSettings[index];
            var editArea = document.getElementById(editAreaSettings.areaPrefix+itemID); 
            if(editArea == null)
               continue;
            //check if there is specific handler on extract data event.     
            if(editAreaSettings.onExtractEditedValue != null)
                editAreaSettings.onExtractEditedValue(retObj, editAreaSettings, editArea);
            else
                //otherwise call predefined event of IEC
                extractEditedValue(retObj, editAreaSettings, editArea);
            
                
       }
       //return the obj that holds new edited values
       return retObj;
       
   
   }
   
  function extractEditedValue(refRetObj, editAreaSettings, editArea)
   {      
          var children = extractDHTMLElements(editArea);
          if(editAreaSettings.areaDataSourceControlID == null)
          {  //save new value  
             refRetObj[editAreaSettings.areaName] = children[1].value;
             //assign new value for readable field
             children[0].innerHTML = children[1].value; 
          }
          else if(children[1].tagName == 'SELECT' )
          {  
             var select = children[1];
             refRetObj[editAreaSettings.areaName] = 
     (trimText(select.options[select.selectedIndex].value)=="") 
    ? null : select.options[select.selectedIndex].value;
             children[0].innerHTML = (trimText(select.options[select.selectedIndex].value)=="") ? 
    "" : select.options[select.selectedIndex].text; 
         }
      
   
   }
 
There is one possible customization at this stage.

onExtractEditedValue
A custom handler may be used to extract edited values from Edit Areas. Example

Stage 4. Disabling Edit Mode

When we saved changes, the table row should be turned back into original state. Not so trivial thing in fact. The cancelEditRow function is responsible for the transition.
function onEditing(eventObj)
   {  
      if(eventObj == null)
         eventObj = event;
      //27 - key code of escape key
      if(eventObj.keyCode == 27)
        onCancelAction();
      ...
   
  }
   function onCancelAction()
   {  
       cancelEditRow();
   }


 function cancelEditRow()
   {
       if(activeInlineEditPanel == null)
          return;
       
       var children = extractDHTMLElements(activeInlineEditPanel);
       for(var index=0; index < children.length; index++)
       {  
          var control = children[index];
          switch(control.attributes[INLINE_ACTION_ATTRIBUTE].nodeValue)
          {
             case INLINE_EDIT_ACTION: control.style.display=''; break;
             case INLINE_SAVE_ACTION: control.style.display='none';break;
             case INLINE_CANCEL_ACTION: control.style.display='none';break;
             default:/* "Do nothing" */; 
          }
       }
       
       highLightInlineEditRow(INLINE_ROW_BACKGROUNDCOLOR_NORMAL);
       var itemID = getActiveInlineEditPanelID();
       if(itemID == null)
       { 
           alert("error: Unable to find item id.");
           activeInlineEditPanel = null;
           return;  
       }
       for(var index=0; index < editAreasSettings.length; index++)   
       { 
          var editAreaSettings = editAreasSettings[index]; 
          var editArea = document.getElementById(editAreaSettings.areaPrefix+itemID);
          if(editArea == null)
             continue;
         
          if(editAreaSettings.onCancelEditValue != null)
          {   
               editAreaSettings.onCancelEditValue(editAreaSettings, editArea);
          }
          else
            {
              if(editAreaSettings.areaDataSourceControlID == null)
                cancelEditTextField(editArea);
              else
                cancelEditDataSourceField(editArea);
            }
          
          
          if(editAreaSettings.onRenderEditedValue!=null)
              editAreaSettings.onRenderEditedValue(editAreaSettings, editArea);
        
       }
       activeInlineEditPanel = null;
   
   }

There are two possible customizations at this stage.

onCancelEditedValue
A custom handler may be used to set edit area into original (read-only) state. Example

onRenderEditedValue
A custom handler may be used to render original (read-only) label with some specialties, like display an image that represents Priority or something like that. Example

TransactObject implementation

TransactObject is a helper object that manages saving procedure (it acts as a synchronizer and restore old values in case of server side error as well as confirm valid saving).

The IEC replaces editable fields by text with new values, while the server is still processing the request. Since updating is asynchronous, we have a problem of web service exception synchronization. What happens when object can't be saved and we receive exception? New values will be set for the row, but these values are not correct anymore.

Another problem is that AJAX.NET wraps all exceptions from web service into WebServerError class and it's impossible to get information about what item was failed. TransactionObject handles these problems. It has a reference to IEC and can restore the initial values in case of error. IEC holds the initial values of the item til commitChanges or abandonChanges method call.
function onSaveEventHandler(retObj)
{
   var transactionObj = new TransactionObject(retObj.itemID, inlineEditController);
   transactionObj.onStatusSuccess = onRequestComplete;
   transactionObj.onStatusError = onRequestError;
   retObj.OrderID  = retObj.itemID;
   TargetProcess.DaoTraining.BusinessLogicLayer.OrderService.UpdateOrder(retObj,
                                     transactionObj.onSaveSuccess ,
                                     transactionObj.onSaveError)
}


function TransactionObject(itemID, inlineEditController, onSuccessStatus, onErrorStatus)
{
  var thisRefTransactionObject = this;
  this.inlineEditController = inlineEditController;
  this.itemID = itemID;
  this.onStatusSuccess = onSuccessStatus;
  this.onStatusError = onErrorStatus;

  //--- Events ---
  this.onSaveSuccess = function(result)
  {
    //remove the initial values for item from IEC. 
    thisRefTransactionObject.inlineEditController.commitChanges(thisRefTransactionObject.itemID);
    //call custom handler if such handler is provided
    if(thisRefTransactionObject.onStatusSuccess)
        thisRefTransactionObject.onStatusSuccess(result);
  
  }

  this.onSaveError = function(ex, context)
  { 
     //restore the initial values for item 
     thisRefTransactionObject.inlineEditController.abandonChanges(thisRefTransactionObject.itemID);
     if(thisRefTransactionObject.onStatusError)
        thisRefTransactionObject.onStatusError(ex);
 
  }
 
 

Extending Base Functionality. Custom Edit Handler.

By default IEC has a simple logic for text and drop down editing, since it is impossible to implement all cases.

Sometimes, you get into situation when the base functionality is not enough. For instance, Priority property of Order class. When the row is in read-only state it must show the priority image and in edit state it must show the select box.
   <asp:TemplateField>
          <ItemTemplate>
            <%--priorityname attribute holds priority value --%>
                <span priorityname='<%#Eval("Priority")%>' id="PriorityIdPrefix<%# Eval("OrderID")%>">
                      <span>
                            <%#GetPriorityHTML(Container.DataItem)%>
                      </span>
                </span>
          </ItemTemplate>
   </asp:TemplateField>

                

var prioritySettings = new EditAreaSettings("Priority","PriorityIdPrefix","uxPriorities");
prioritySettings.onRenderEditedValue = onRenderPriorityValue;
prioritySettings.onSelectValue = onSelectPriorityValue;
editAreasSettings.push(prioritySettings);
var inlineEditController = new InlineEditController('<%=uxOrders.ClientID%>', editAreasSettings, onSaveEventHandler, true);
var  HIGHEST_PRIORITY_IMAGE =  "<%=HIGHEST_PRIORITY_IMAGE%>";
var  NORMAL_PRIORITY_IMAGE  =  "<%=NORMAL_PRIORITY_IMAGE %>";
var  LOWEST_PRIORITY_IMAGE  =  "<%=LOWEST_PRIORITY_IMAGE %>";
var  HIGHEST_PRIORITY = "<%=Priority.Highest %>";
var  NORMAL_PRIORITY  = "<%=Priority.Normal %>";
var  LOWEST_PRIORITY  = "<%=Priority.Lowest%>";
var  PRIORITY_ATTR  = "priorityname";

It is required to provide two handlers: onRenderPriorityValue and onSelectPriorityValue.

onSelectPriorityValue handler just selects the predefined value in select box.

function onSelectPriorityValue(editAreaSettings, editArea)
{

   var dataSourceControl = document.getElementById(editAreaSettings.areaDataSourceControlID);
   dataSourceControl.value =  editArea.attributes[PRIORITY_ATTR].nodeValue;

}

onRenderPriorityValue converts Priority value into specific image.
function onRenderPriorityValue(editAreaSettings, editArea)
{

    var dataSourceControl = document.getElementById(editAreaSettings.areaDataSourceControlID);
    var children = extractDHTMLElements(editArea);
    var readField = children[0];
    switch(dataSourceControl.value)
    {
        case HIGHEST_PRIORITY:
            readField.innerHTML = HIGHEST_PRIORITY_IMAGE;
        break;
        case NORMAL_PRIORITY:
           readField.innerHTML =  NORMAL_PRIORITY_IMAGE;
        break;
        case LOWEST_PRIORITY:
           readField.innerHTML =  LOWEST_PRIORITY_IMAGE;
        default:/*Do nothing*/   
    }
    editArea.attributes[PRIORITY_ATTR].nodeValue = dataSourceControl.value;

}


This situation is quite an unique case. In most case the base logic of IEC will fit your requirements. For instance, Country property of Order class works just fine.

 <asp:TemplateField HeaderText="Ship Country">
  <ItemTemplate>
   <span id="ShipCountryIdPrefix<%# Eval("OrderID")%>">
    <asp:Label ID="Label1" runat="server" Text='<%#Eval("ShipCountry")%>'></asp:Label>
   </span>
  </ItemTemplate>
 </asp:TemplateField>
 
 .....
 
  <select style="display: none" id="uxCountries">
  <option>-- Select Country</option>
  <option value="USA">United States</option>
  <option value="Canada">Canada</option>
  <option value="Mexico">Mexico</option>
  <option value="Afghanistan">Afghanistan</option>
  <option value="Albania">Albania</option>
 ....
 
  
var shipCountrySettings  = new EditAreaSettings("ShipCountry","ShipCountryIdPrefix","uxCountries");
editAreasSettings.push(shipCountrySettings);
   

As you see, there are no custom handlers. All what you need to do is to pass "uxCountries" ID into shipCountrySettings. The IEC will successfully select the value in select box using the text in column and render the new value.

Extending Base Functionality. CheckBox Handler.

What if you have boolean value that is represented as a checkbox in the grid? Let's see how that can be implemented. <
 <asp:TemplateField HeaderText="Rush Order">
  <ItemTemplate>
       <span id="IsRushOrderIdPrefix<%#Eval("OrderID")%>" >
          <asp:CheckBox  Enabled="false" runat="server" Checked='<%#Eval("isRushOrder") %>' ID="uxRushOrder" />
       </span>
  </ItemTemplate>
 </asp:TemplateField>
     

var isRushOrderSettings = new EditAreaSettings("IsRushOrder","IsRushOrderIdPrefix");
 isRushOrderSettings.onEditValue = onEditRushValueHandler;
isRushOrderSettings.onExtractEditedValue = onExtractRushValueHandler;
isRushOrderSettings.onCancelEditValue = onCancelEditRushValueHandler;
editAreasSettings.push(isRushOrderSettings);

I should create three custom handlers: onEditRushValueHandler, onExtractRushValueHandler, and onCancelEditRushValueHandler. onEditValue just enables checkbox.

function onEditRushValueHandler(editAreaSettings, editArea)
{

    var isRushOrderSpan = getFirstDHTMLElement(editArea);
    isRushOrderSpan.disabled = false;
    var isRushOrderChk = getFirstDHTMLElement(isRushOrderSpan);
    isRushOrderChk.disabled = false;
} 
onExtractRushValueHandler gets the new value from checkbox and save it in retObj (that is passed into onSaveEventHandler as you remember).

function onExtractRushValueHandler(retObj, editAreaSettings, editArea)
{
    var isRushOrderChk = getFirstDHTMLElement(getFirstDHTMLElement(editArea));
    retObj[editAreaSettings.areaName] = isRushOrderChk.checked;
}

onCancelEditValue disables checkbox.

function onCancelEditRushValueHandler(editAreaSettings, editArea)
{
    var isRushOrderSpan = getFirstDHTMLElement(editArea);
    isRushOrderSpan.disabled = true;
    var isRushOrderChk = getFirstDHTMLElement(isRushOrderSpan);
    isRushOrderChk.disabled = true;
}

Thus you can create custom handlers for any case, even extremely complex.

Input Validation

There are no standard solutions to validate input in javascript (like ASP.NET validators, for example), but you still can use handlers that would act as validators. Fortunately, TransactObject restores the initial values in case of server side error that can be caused by invalid input, but it's not enough. We should implement extra validation for the Freight property that is supposed to be Decimal to validate input.
   <asp:TemplateField HeaderText="Freight">
  <ItemTemplate>
   <span id="FreightIdPrefix<%# Eval("OrderID")%>">
        <asp:Label ID="Label1" runat="server" Text='<%#Eval("Freight")%>'></asp:Label>
   </span>
  </ItemTemplate>
 </asp:TemplateField>
 
 var freightSettings = new EditAreaSettings("Freight","FreightIdPrefix");
  freightSettings.onSelectValue = onSelectFreightValueHandler;
  editAreasSettings.push(freightSettings);
We need to implement just one handler onSelectFreightValueHandler. This handler assign validation handler for Freight input box and it is impossible to type anything except numbers and decimal separator.
  


  function onSelectFreightValueHandler(editAreaSettings, editArea)
  {
   var children = extractDHTMLElements(editArea);
    //children[0] - readable field that gets invisible since the row is in editable state
    //children[1] - input field that gets added since the row is in editable state
 children[1].onkeypress = onKeyPressDecimalValueHandler;
  }

  function onKeyPressDecimalValueHandler(eventObj,obj)
  { 
    var thisRef;
    if(obj == null)
      thisRef = this;
    else
      thisRef = obj;

    function isValidInput(key,value)
    {
        if(key == "." && value.indexOf(".")==-1)
          return true;
        if(isNaN(key))
           return false;
        else
           return true;  
        
    }

    if(typeof event == "undefined")
    {
        var key =  String.fromCharCode(eventObj.charCode);
        if(eventObj.charCode == 0)
           return;
        if(!isValidInput(key, thisRef.value))
           eventObj.preventDefault();
    }
    else
    {
        var key =  String.fromCharCode(event.keyCode);
        if(!isValidInput(key, thisRef.value))
           event.returnValue = false;
    }
  
     
}
The other solution is to use TransactObject as a validator. For instance, if I try to enter invalid date TransactObject restores the initial value and shows the error message.

Now you can use inline editing in all your lists without problems. Let us know if any questions!

Labels: , ,

Oct 23, 2007 / Grid Inline Editing Using ASP.NET AJAX and Web Services

Introduction

If you ask people what is the most important thing for web based application UI most of them will answer "I want to edit data really fast. Like in Excel!" That a tough request. It means that person wants to click on a row, edit some data, then click on another row and have all data saved into database. We've found a solution during TargetProcess v.2.6 development. TargetProcess is a great agile project management software (if you are not aware ;)

Problem

ASP.NET framework provides inline editing functionality in GridView. But sometimes you get into situation when you cannot use this functionality:

  • You cannot use it with disabled View State
  • Performance is really bad. When you click button to switch the row into edit state, post back to the server will be initiated. Post back will happen when you save or cancel edit mode as well. Even if you put your grid into UpdatePanel, AJAX.NET still will update whole grid instead of one row and size of the date in post back remains almost the same (and large).

With bad performance it will not be like Excel, right? So we have to look for solution that:

  • Will not initiate post back to the server
  • Will work with disabled view state
  • Will be as fast as Excel (at least less than a second to make the action)

Solution

The Inline Edit Controller (IEC) is javascript solution that resolve all our problems (you will see it really soon). It has the following advantages:

  • It's a cross-platform solution, you can use it in any environment ASP, ASP.NET, PHP, JSP etc.
  • It's a cross-browser solution
  • It's a very extensible solution, it's pretty simple to extend the basic behavior by adding your custom handlers.
  • It can be easy integrated into existing applications without almost any rework. You can define custom saving methods to put edited values into database.
  • Basic principles

IEC is a plain javascript solution that allows editing rows in grid. In order to have editable row you should mark the row in some specific manner. Let's call this stuff "Inline Edit Panel". It contains a button that enables inline editing as well as buttons that save change and cancel editing. In fact the better way to go is to enable editing on double click and cancel it on Escape key. That will be shown in more advanced examples. So far we have simple buttons.

<asp:GridView GridLines="none" runat="server" AllowSorting="false" AutoGenerateColumns="false" ID="uxOrders">

  <asp:TemplateField>
       <ItemTemplate> 
            <span runat="server" class="inlineEditIcon" inlineeditattribute="true" rowid='<%# Eval("OrderID")%>'>
              <img runat="server" action="edit" title="Inline Edit" src="~/img/edit.gif" />
              <img runat="server" style="display: none" action="save" title="Save" src="~/img/save.gif" /> 
              <img runat="server" style="display: none" action="cancel" title="Cancel" src="~/img/cancel.gif" />

          </span>
     </ItemTemplate>
</asp:TemplateField>
... 

Cells that are going to be editable must be located in a specific place holder as well. Let's call this stuff "Edit Area".

<asp:TemplateField HeaderText="Freight">
     <ItemTemplate>

          <span id="FreightIdPrefix<%# Eval("OrderID")%>">
        <asp:Label ID="Label1" runat="server" Text='<%#Eval("Freight")%>'></asp:Label>
        </span>
    </ItemTemplate>

</asp:TemplateField>

As you see, "Inline Edit Panel" has 3 buttons with an extra "action" attribute. Each action attribute fires corresponding event. IEC handles all these events and makes corresponding changes in DOM. Let's review all editing process:

1. User clicks Edit button

  • At this stage the row switches to editable state and input and select boxes appended to "Edit Areas";
  • Usual text labels become invisible. It means that Freight label in the code above becomes invisible and input text field added to the span with id="FreightIdPrefix777"

2. User changes some values and clicks Save button.

  • At this stage IEC extracts all values from editable fields (input, select, other if any);
  • Replace the labels text with new values from editable fields;
  • Create javascript object that holds the edited (new) values.
  • It's time to save changes into database. IEC has no idea how to save new data, so it just passes the javascript object that holds new values into saving method.

3. User pushes Cancel button

  • At this stage IEC removes editable fields ("input", "select");
  • Labels become visible again

Usage Example

Idea looks really simple. But maybe the solution is hard to use, who knows… Only real example can help us to judge the solution.

Let's take ASP.NET example with:

  • GridView control to present a data.
  • Web Service to retrieve the data and save modified data.
  • AJAX.NET to generate web methods that can be used from javascript.

1) For example, we have a pretty simple Order class. We want to show all orders in a grid.

2) GridView initialization is no-brainer as well. OrderService is a class that can retrieve orders from database (we don't care how, maybe GetAllOrders method uses NHiberate or maybe old plain SQL)

protected override void OnLoad(EventArgs e)
{
  if (!IsPostBack)
  {
     OrderService orderService = new OrderService();
     Order[] orders = orderService.GetAllOrders();
     uxOrders.DataSource = orders;
     uxOrders.DataBind();
...

3) IEC mapping. As mentioned above, we should add columns with inline editing controls (Inline Edit Panel).

<asp:TemplateField>
      <ItemTemplate> 
       <span id="Span1" runat="server" style="white-space: nowrap" class="inlineEditIcon" inlineeditattribute="true"  
          rowid='<%# Eval("OrderID")%>'> 
          <img id="Img1" runat="server" action="edit" title="Inline Edit" src="~/img/edit.gif" /> 
          <img id="Img2" runat="server" style="display: none" action="save" title="Save" src="~/img/save.gif" />

          <img id="Img3" runat="server" style="display: none" action="cancel" title="Cancel"    
          src="~/img/cancel.gif" />                       
        </span> 
     </ItemTemplate> 
</asp:TemplateField> 

And insert all required "Edit Areas".

<asp:GridView GridLines="none" runat="server" AllowSorting="false" AutoGenerateColumns="false" ID="uxOrders">
      <Columns>
          <asp:TemplateField>
               <ItemTemplate> 
                   <span priorityname='<%#Eval("Priority")%>' id="PriorityIdPrefix<%# Eval("OrderID")%>">

                     <span>
                         <%#GetPriorityHTML(Container.DataItem)%>
                     </span
               </span>
             </ItemTemplate>
         </asp:TemplateField>
         <asp:TemplateField HeaderText="Freight">

               <ItemTemplate>
                   <span id="FreightIdPrefix<%# Eval("OrderID")%>">
                   <asp:Label ID="Label1" runat="server" Text='<%#Eval("Freight")%>'></asp:Label>
                </span>

            </ItemTemplate>
         </asp:TemplateField>
          <asp:TemplateField HeaderText="Ship Name">
                <ItemTemplate>
                       <span id="ShipNameIdPrefix<%# Eval("OrderID")%>">

                        <asp:Label ID="Label1" runat="server" Text='<%#Eval("ShipName")%>'></asp:Label>
                   </span>
             </ItemTemplate>
         </asp:TemplateField> 
...

As you see, the "Edit Areas" have composite id that consists of two parts "Edit Area prefix + itemID". Such id is required to make each row unique.

4) Now we should create IEC instance and the EditAreaSettings instances (see below) for each "Edit Area".

var editAreasSettings = new Array();
//ShipName is a property of Order class
//ShipNameIdPrefix is an id of span element(EditArea of ShipName property)   
var shipNameEditAreaSettings = new    EditAreaSettings("ShipName","ShipNameIdPrefix",null, true);
editAreasSettings.push(shipNameEditAreaSettings);
 
var freightSettings = new EditAreaSettings("Freight","FreightIdPrefix");
freightSettings.onSelectValue = onSelectFreightValueHandler;editAreasSettings.push(freightSettings);
 
var prioritySettings = new EditAreaSettings("Priority","PriorityIdPrefix","uxPriorities");
prioritySettings.onRenderEditedValue = onRenderPriorityValue; 
prioritySettings.onSelectValue = onSelectPriorityValue; editAreasSettings.push(prioritySettings);

var inlineEditController = new InlineEditController('<%=uxOrders.ClientID%>', editAreasSettings, onSaveEventHandler, true);

new EditAreaSettings( areaName, areaPrefix , dataSourceControlID, isFocused)
arguments:
areaName – used by IEC to map edited values to the retObj that is passed into onSaveEventHandler
areaPrefix – used by IEC to find the editArea
dataSourceControlID – data source control id that has a collection of predifined values
isFocused - boolean value that specifies whether edit Area will have a focus.

onRenderPriorityValue and onSelectPriorityValue are custom handlers that implemented outside of IEC. It impossible to implement all cases of editing and for specific situations you have to create custom handlers. For example, Priority column is an image. When row is in usual state it shows the priority icon, but when the row is in editable state it should show the select box.

The code of onRenderPriorityValue, onSelectPriorityValue handler are out of scope for this article, it will be described in Part II (architecture, extensibility and stuff like that).

5) We are getting closer to the final. Let's add saving handler. IEC is quite agnostic to data saving and does not care how you will implement that. All it needs is a method call that do all the job.

function onSaveEventHandler(retOb)
 { 
    retObj.OrderID  = retObj.itemID;
    TargetProcess.DaoTraining.BusinessLogicLayer.OrderService.UpdateOrder(retObj,
                                       onRequestComplete,
                                       onErrorRequest);
 }

UpdateOrder method will be fired on save event. It has only one input parameter retObj which holds all edited values. IEC maps the edited values into retObj using the keys from EditAreaSettings (for example, shipName, Freight ). Fortunately, I maped the "Edit Areas" for IEC to be consistent with Order class except retObj.itemID that is created by IEC. As a result retObj can be casted to Order class. I had to do one extra assignment to make retObj completely consistent with Order

retObj.OrderID = retObj.ItemID

onSaveEventHandler handler is completely consistent with web method.

[ScriptService]
 public class OrderService : WebService
 {
 
   [WebMethod]
   public string UpdateOrder(Order order)
   {
            OrderDao OrderDao = DaoFactory.GetDaoFactory().GetOrderDao();
            bool isUpdated = OrderDao.UpdateOrder(order);
            if (isUpdated)
                return "The order '" + order.OrderID + "' is successfully updated.";
            else
                return "Unable to find order '" + order.OrderID + "'.";
   }
 …

I'm using the web service to save the order, but it's not necessary. It's a pretty flexible approach that allows other mechanism to save the data.

In part II I will show inner architecture of Inline Edit Controller as well as some specific features and customizations.

Labels: , , , , ,



TP v.2 online demo


© 2004-2007 TargetProcess. Agile project management software
info@targetprocess.com