An Actual Reorder List Control with Substance Part 2
11/6/2007 10:09:57 PM

In the last post I introduced the functionality to create a spiffy drag-and-drop / Reorder List control using C#, XML, and JavaScript. I didn't divulge all the code yet and will explain a little more about what I did in this post. First I will start with the creation of the textboxes and labels.
private
void CreateControls()
{
ProductGroup.RetrieveAll().ForEach(
delegate(ProductGroup productGroup)
{
int counter = 0;
Label productGroupName = new Label();
productGroupName.Text = productGroup.ProductGroupName;
productGroupName.CssClass = "labelText";
productGroupName.Width = 120;
productGroupName.ID = string.Format("display{0}", productGroup.ProductGroupId);
Label productGroupTotals = new Label();
productGroupTotals.Text = "0";
productGroupTotals.CssClass = "labelText";
productGroupTotals.ID = string.Format("total{0}", productGroup.ProductGroupId);
ph.Controls.Add(new LiteralControl(string.Format("\t\t<tr>{0}", Environment.NewLine)));
ph.Controls.Add(new LiteralControl(string.Format(string.Format("\t\t\t<td valign=\"top\">{0}\t\t\t", Environment.NewLine))));
ph.Controls.Add(productGroupName);
ph.Controls.Add(new LiteralControl(string.Format("{0}\t\t\t", Environment.NewLine)));
ph.Controls.Add(productGroupTotals);
ph.Controls.Add(new LiteralControl(string.Format("{0}\t\t\t</td>{0}", Environment.NewLine)));
foreach (Product product in productGroup.Products)
ph.Controls.Add(new LiteralControl(string.Format("\t\t\t\t<td class=\"product\"><input type=\"text\" readonly=\"readonly\" id=\"productgroup_{0}_product_{1}\" onfocus=\"javascript:selectAll('productgroup_{0}_product_{1}');\" ondragenter=\"window.event.returnValue = false\" ondragover=\"window.event.returnValue = false\" ondrop=\"onDropEvent({0}, 'productgroup_{0}_product_{1}', '{2}')\" ondragstart=\"onDragStartEvent({1}, {0}, 'productgroup_{0}_product_{1}')\" class=\"productText\" value=\"{2}\" /></td>{3}", productGroup.ProductGroupId, ++counter, product.ProductId, Environment.NewLine)));
ph.Controls.Add(new LiteralControl(string.Format("\t\t</tr>{0}", Environment.NewLine)));
});
}
This looks pretty intimidating but it really isn't at all. I choose to create my controls programmatically because I like having control of all the attributes (i.e. onblur, ondragstart, ondrop, onfocus, etc...). Enumerating a collection of objects gives me direct access to every property that I could possibly need. If this functionality were part of a page that could be posted back I would have overridden CreateChildControls(). That way these controls would have been added to the Page's control tree and maintained state during postbacks. I'll use this opportunity to plug how cool the CreateChildControls override can be.
The labels are created to display the ProductGroup name as well as (for this first example) the sum of the Product prices.
Side note: I add a ton of tabs (\t) as well as Environment.NewLine because I like the output HTML to look neat and tidy. This is a personal preference and you can get rid of this part of the code if you want. I will say that when doing complex functionality it makes troubleshooting a LOT easier.
JavaScript Variables
var
productNode = null; // product being dragged
var productGroupNode = null; // productgroup for productNode
var globalProductGroupId = null; // productgroup being dragged
var globalProductGroupIdDrop = null; // productgroup dropped into
var selectedProductControlId = null; // control that is selected
var droppedProductControlId = null; // control that is dropped into
var xmlHttpRequest = null; // for database interaction later on
Pertinent JavaScript Functions & Methods
function
selectAll(controlId)
{
document.getElementById(controlId).focus();
document.getElementById(controlId).select();
selectedProductControlId = controlId;
}
The selectAll function simply selects the value in the textbox and enables it for dragging. This is here for nothing more than providing ease-of-use to the end-user. You could just have a free-form textbox and allow them to select the values. This can be problematic if you have multiple digit values.
function
clearTextBoxes(productGroupId)
{
for (i = 0; i < 4; ++i)
{
var id = (i+1) * 1;
var controlId = "productgroup_" + productGroupId + "_product_" + id;
document.getElementById(controlId).value = "";
}
}
This method simply clears the old values out of the textboxes. There are 4 textboxes in each row (hence the 4 in the for-loop). Inside the for-loop I am concatenating the productGroupId (which happens to be the textbox) to my existing naming convention for the textboxes we created above. I then set this textbox's value to empty.
function
populateTextBoxes(productGroupId, productGroupNode)
{
var products = productGroupNode.getElementsByTagName("product");
for (i = 0; i < products.length; ++i)
{
var id = (i+1) * 1;
var controlId = "productgroup_" + productGroupId + "_product_" + id;
document.getElementById(controlId).value = products[i].attributes[0].nodeValue;
}
}
Kind of like the method above except for now we are filling the textboxes with the new value from the ProductGroup node. The productGroupId tells me what row it is on. I then enumerate the contents of the products node and perform the same motions as above, except that now I am filling the contents with an actual value.
function
onDragStartEvent(productId, productGroupId, controlId)
{
globalProductGroupId = productGroupId*1;
productNode = xmlData.selectSingleNode("//productGroup/product[@productId='" + document.getElementById(controlId).value + "']");
}
Start of the drag event. I set my global variable (so I can access it later) as well as my ProductNode (also used later). Notice the XPath syntax.
function
onDropEvent(productGroupId, controlId, productIdDrop)
{
productGroupNode = xmlData.selectSingleNode("//productGroup[@productGroupId='" + productGroupId + "']");
var productGroupNodeOld = xmlData.selectSingleNode("//productGroup[@productGroupId='" + globalProductGroupId + "']");
var productNodeOld = xmlData.selectSingleNode("//productGroup/product[@productId='" + document.getElementById(controlId).value + "']");
productGroupNode.appendChild(productNode);
productGroupNodeOld.appendChild(productNodeOld);
reorderProducts(productGroupNode);
reorderProducts(productGroupNodeOld);
clearTextBoxes(globalProductGroupId);
clearTextBoxes(productGroupId);
populateTextBoxes(globalProductGroupId, productGroupNodeOld);
populateTextBoxes(productGroupId, productGroupNode);
productNode = null;
calculateProductGroupData();
}
This is where it gets really fun (complicated for some). So earlier we set the Product node in the drag event. Here we select the ProductGroup and Product node that were dropped into as well as the old ProductGroup (dragged from). I guess I could've added the dragged ProductGroup node in the dragstart but that's neither here nor there in this basic tutorial. Now I am appending the children to the appropriate parent nodes in the Xml document (xmlData). For a better user-experience I call my reorderProducts function so that the products will be numerically sorted lowest to highest. This is optional, but usually a requirement (has been) every time I add functionality like this to a client's project.
Reset the textboxes (clearTextBoxes) and then repopulate them (populateTextBoxes) which are both explained earlier in this entry.
function numericalSort(a, b) { return (a - b); }
Simple way to sort numerically (lowest to highest). This can be reversed (b - a) or not even used at all. Like I said earlier, most people (clients) required this in the past.
function
calculateProductGroupData()
{
var products = null;
var productId = -1;
var productPrice = -1;
var productGroupId = -1;
var productGroups = xmlData.getElementsByTagName("productGroup");
for ( i = 0; i < productGroups.length; i++ )
{
globalSubTotal = 0;
productGroupId = productGroups[i].attributes[0].nodeValue;
if ( productGroups[i].hasChildNodes )
{
products = productGroups[i].getElementsByTagName("product");
for ( p = 0; p < products.length; p++ )
globalSubTotal += products[p].attributes[2].nodeValue*1;
document.getElementById("total" + productGroupId).innerHTML = "$" + globalSubTotal;
}
}
}
The meat-and-potatoes of the application if you will. This lets users know that something is happening. Upon each successfull drop these values will change. This method will enumerate the newly-changed Xml document and recalculate everything.
For each ProductGroup node in the Xml document we are seeing if there any child nodes available. If there are (for this example there ALWAYS will be) we are then enumerating the child nodes and calculating the data, in this case the subtotal (price).
This is pretty straight-forward.
function
reorderProducts ( productGroupNode )
{
var productGroupNodeId = productGroupNode.attributes[0].nodeValue;
var products = productGroupNode.getElementsByTagName("product");
var productItems = new Array(products.length);
for ( p = 0; p < products.length; p++ )
productItems[p] = products[p].attributes[0].nodeValue;
productItems.sort(numericalSort);
for ( i = 0; i < productItems.length; i++ )
{
var node = xmlData.selectSingleNode("//productGroup/product[@productId='" + productItems[i] + "']");
productGroupNode.appendChild(node);
}
}
This looks crazy but it's rather simple. All I'm doing is passing a ProductGroup node and creating a new array that happens to be the length of that node (in our case 4). I then add the items from the node to said array in a for-loop. The JavaScript array has a sort method but defaults (I believe) to alphanumeric sorting. Since we are ONLY dealing with integers we could use this but I felt I should provide this in the event that you actually need it.
After the array is sorted to our liking we have another for-loop where we create the nodes (children) and append back to the appropriate parent.
Sorry for not posting this earlier. Again, VERY busy with both work and play. Hopefully this post coupled with the last will keep you busy for a little while. Feel free to post questions and I will try my best to answer them.
In the next entry relating to this I will introduce database interaction making it a complete AJAX application.
Related
An Actual Reorder List Control with Substance Part 1
AJAX,
C#,
JavaScript
