|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Read Part I here.
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 a real project - TargetProcess - ASP.NET based agile project management software. This part describes the architecture and advanced features like validation and extension. Basic ClassesInline editing solution consists of three classes:
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 the classes. InlineEditControllerController class that handles inline editing. var inlineEditControllerObj = new InlineEditController
(gridID, editAreasSettings , onSaveEventHandler, isExplicitSave)
Arguments
Public Methods
EditAreaSettingsClass that stores information about what should be processed in IEC. The IEC should have information about what and how it should be updated. The var editAreaSettingsObj = new EditAreaSettings(areaName,
areaPrefix,
areaDataSourceControlID,
isFocus,
onSelectValue,
onExtractEditedValue,
onRenderEditedValue,
onCancelEditValue,
onEditValue)
Arguments
All these handlers can be omitted. You should use them for complex cases. TransactObjectHelper object that manages saving procedure (it acts as a synchronizer and restores old values in case of server side error as well as confirms valid saving). var transactionObject = new TransactionObject(itemID, inlineEditController);
Arguments
IEC LifeCycleIEC 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 InitializationIEC 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 ModeWhat happens when the user clicks Edit link? Maybe the system crashes as usual? Not at this time! It sets the table row into editable state. The most interesting method is
//********************* 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 predefined handler of IEC
selectValueInDataSourceControl(editArea);
if(editAreaSettings.isFocus)
setFocusForEditableField(editArea);
}
}
There are two possible customizations at this stage:
Stage 3. Save ChangesFirst we should extract new values from form fields. The methods with very original names 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.
Stage 4. Disabling Edit ModeWhen we saved changes, the table row should be turned back into original state. Not such a trivial thing in fact. The 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.
TransactObject Implementation
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 an object can't be saved and we receive an 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 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 HandlerBy default IEC has a simple logic for text and drop down editing, since it is impossible to implement all cases. Sometimes, you get into a situation when the base functionality is not enough. For instance,
<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:
function onSelectPriorityValue(editAreaSettings, editArea)
{
var dataSourceControl =
document.getElementById(editAreaSettings.areaDataSourceControlID);
dataSourceControl.value = editArea.attributes[PRIORITY_ATTR].nodeValue;
}
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 a unique case. In most case the base logic of IEC will fit your requirements. For instance, <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 Extending Base Functionality. CheckBox HandlerWhat if you have a 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: function onEditRushValueHandler(editAreaSettings, editArea)
{
var isRushOrderSpan = getFirstDHTMLElement(editArea);
isRushOrderSpan.disabled = false;
var isRushOrderChk = getFirstDHTMLElement(isRushOrderSpan);
isRushOrderChk.disabled = false;
}
function onExtractRushValueHandler(retObj, editAreaSettings, editArea)
{
var isRushOrderChk = getFirstDHTMLElement(getFirstDHTMLElement(editArea));
retObj[editAreaSettings.areaName] = isRushOrderChk.checked;
}
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 ones. Input ValidationThere are no standard solutions to validate input in JavaScript (like ASP.NET validators, for example), but you can still use handlers that would act as validators. Fortunately, <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 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 Now you can use inline editing in all your lists without problems.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||