<3 this little jQuery gem
$(document).ready(function() {
$('#button').data('Data', { Message: 'sup foo?', Email: 'asdf@asdf.com' });
$('#button').click(function() {
var data = $(this).data('Data');
alert(data.Message + '\n' + data.Email);
});
});
<input type="button" id="button" value="clickie" />

Easy.
Tags:
JavaScript,
jQuery
I've been using a lot of jQuery lately in a new project and am falling in love with it! It is a wonder why I have never used it before but am glad I was kind of forced to learn it ;)
As with anything new there is a bit of a learning curve. The application I am working on boasts a large number of tabs for different sections of the site. One of the requirements is to toggle the "active" tab via CSS class. Easy right? This was easy to do in vanilla JavaScript so it should be super-easy to do with jQuery. It is!
$('#FooItem1').removeClass('active');
$('#FooItem2').removeClass('active');
$('#FooItem3').removeClass('active');
$('#FooItem4').removeClass('active');
$('#FooItem5').removeClass('active');
That works great but there is one caveat: adding new tabs. If the requirements of the UI changed and we were to have to add a new tab (or 6 more) then we would have to not only change the view (or in this case partial view ;) ) but also the JavaScript functions dealing with these tabs. Since all of our tabs are UL with stylized LI's containing anchor tags I decided to create something like this:
function
RemoveActiveClassFromListItemControlByID(controlID) {
$("#" + controlID).children().each(function() {
$(this).children("a").removeClass("active");});
}
Now this isn't that generic since I have the "a" and "active" strings hard-coded. In this case it works for us since all the tabs are the same format and all we really need is the id of the control. This little function will enumerate the children of the control (in this case all LI's and then enumerate the A children and remove the active css class. Simple. To extend this to be even more generic you could do the following:
function
RemoveClassNamesFromChildElementByControlID(controlID, childElement, className) {
$("#" + controlID).children().each(function() {
$(this).children(childElement).removeClass(className);});
}
Here's an example of how you could use the function above. Let's say that you want to enumerate all children in a OL with an id of "fooList". Each children has a span tag with a css class of BAR and you want to reset all of them when a user clicks on a hyperlink. This would be extremely simple:
<
a href="javascript:(RemoveClassNamesFromChildElementByControlID('fooList', 'span', 'BAR'));">CLICK HERE</a>
Easy. I'm really starting to love this jQuery business.
jQuery + MVC = love
Tags:
JavaScript
I was working on a personal project of mine tonight that has been in the works for a couple of months now. One piece of functionality required having nested Repeaters. I had my outer repeater, a nested repeater, and then a repeater inside of that repeater for a total of three. At first that sounds like a lot of data, which it is. The idea is that I would only display the top-level data (outer repeater) and have a link for "Expand" to view the nested repeater and within this repeater expand even more. You get the idea. Think of an outer repeater displaying department stores, the nested repeater would have departments in that department store and the nested repeater for the departments in the store might have sales data for a given time period. FWIW, I'm using JavaScript calendar pop-ups to select date ranges as a possible way to filter the data. For this example I will simply show how to display one nested repeater with simply a "view" link.
In the past I've accomplished this functionality with a bunch of postbacks. Lately I've been on this kick where, if at all possible; I will try to not have any unnecessary postbacks. Since my JavaScript game is impeccable (not really), I decided to tackle this. Again, coupled with Control.ClientID I figured this would be a breeze. It was.
<
asp:Repeater ID="repeater" OnItemDataBound="ItemDataBound" runat="server">
<ItemTemplate>
<span><%# Eval("Foo") %></span>
<span><%# Eval("Bar") %></span>
<span><asp:Label ID="view" Text="View" runat="server" /></span>
<div id="details" style="display: none;" runat="server">
<asp:Repeater ID="nested" runat="server">
<ItemTemplate>
<div style="text-align: left;">
<span><%# Eval("Hai") %></span>
<span><%# Eval("LOL") %></span>
<span><%# Eval("ROFL") %></span>
</div>
</ItemTemplate>
</asp:Repeater>
</div>
</ItemTemplate>
</asp:Repeater>
Simple asp:Repeater web control. The ItemDataBound event is where we are going to add pertinent code. I'll get to that in a minute. Notice how I have a div wrapping my nested repeater? This is important to grasp. In my post the other day I mentioned how display: none is better than visibility: hidden when it comes to styling elements of your page. With display: none the elements are ignored when the page is created (i.e. as if they are not there). With visibility: hidden the elements are added to the flow of the page even though you can't visibly see them. With a large set of data (or small) your real estate will diminish rather quickly. In the case above, when the nested repeater is bound it won't be calculated into the flow of the page.
protected
void ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
Foo foo = e.Item.DataItem as Foo;
Label view = e.Item.FindControl("view") as Label;
HtmlGenericControl div = e.Item.FindControl("details") as HtmlGenericControl;
Repeater repeater = e.Item.FindControl("nested") as Repeater;
if (foo != null &&
repeater != null &&
view != null &&
div != null)
{
view.Attributes.Add("onclick", string.Format("javascript:(display('{0}')", div.ClientID));
repeater.DataSource = _cachedListOfT.FindAll(
delegate(FooDetails match)
{
return match.FooId.Equals(foo.FooId);
});
repeater.DataBind();
}
}
}
Here we are casting the current data Item to type Foo and searching for our controls. It is always wise to use the as operator instead of a direct cast. You can never guarantee those controls will be there. A designer could go in and try to be a hero one day and the next thing you know your application just shit itself. The JavaScript function is added to the onclick event of our asp:Label. I am then setting the DataSource of the nested Repeater to a List<T> of a cached collection of objects and using a predicate by way of a delegate to filter out pertinent data. There could probably be a better way to do this but I'm lazy and this is only a proof-of-concept.
function
display(repeaterId)
{
document.getElementById(repeaterId).style.display = "";
}
I'm not even going to explain that JavaScript. Actually, yes. The intuitive reader might be wondering why I have an empty string for the display. It should display as "inherit" or "block" but for some reason no one in the browser development world can agree on anything so if you set it to nothing the browser will display it by default because in trying to determine what an empty string means the browser comes to a point where it simpy says, "F**k it." At least that's how I read why it works this way.
In my real-life version of this I determine the css display style with JavaScript and change the text to "Hide" and clicking on the link again will hide the div. I didn't want to confuse people so I'll just keep it simple.
Have fun with this (or not). Also, this can be extended to display the nested data asynchronously. The onclick event of the "view" link could make an asynchronous request via XmlHttpRequest or ClientCallback and return a string of HTML with your data and then set the innerHTML of the details div to this value. Easy.

Tags:
C#,
JavaScript
This came up this week on a project I am working on and now it will transcend to other projects. I needed to rewrite some functionality utilizing client-side code. Well, I could've done it server-side but I'm not a fan of unnecessary postbacks (or UpdatePanel "partial postbacks") so I decided to utilize JavaScript.
So I removed all the AutoPostBack="true"'s from the codebase and utilized my favorite property (Control.ClientID) to set the css display style to "none" or "" depending on which option was selected. After all that was done I decided to test. Uh-oh, my RequiredFieldValidators were firing off since I was only using CSS* to hide / unhide my Panels. Instead of adding values to these controls to satisfy the expressions (which doesn't work all of the time (i.e. input type='file' or asp:FileUpload which don't allow the setting of a posted file as it is a security risk). I found out how to turn off validation. It only took a little bit to add via codebehind.cs.
For this example, I have a CheckBox for checks that when clicked needs to disable a RequiredFieldValidator for CreditCard. Stupid example I know.
check.Attributes.Add("onclick", string.Format("disable('{0}')", regexCreditCard.ClientID));
That will add the appropriate handler for client-side work. Now that that is out of the way, time for the actual disabling of the validator.
function
disable(validatorId)
{
var validator = document.getElementById(validatorId);
ValidatorEnable(validator, false);
}
Easy. Now you can use .NET to implement tamper-proof validation and disable / enable via JavaScript for a richer user experience.
* Using "display: none" is much better than simply using "visibility: hidden" for what's it's worth (at least it was for me). With "visibility: hidden" you can tell that something is supposed to be there. With "display: none" you can't. So in summary, "visibility: hidden" results in the page calculating this div as part of the flow. With "display: none", it's not even accounted for. It's as if it were never there!
The more you know ;-)
Tags:
AJAX,
JavaScript
Lately I've been on this kick where I try to use basic CSS and JavaScript to mimic functionality available in the ASP.NET AJAX Library. It's not that I am trying to recreate the wheel, just that when I am able to, I get a kick out of recreating functionality that I (or my reader's) might find themselves using in the future. I also think it's funny that some of the controls (watermarked textbox for example) are considered Ajax. It's actually called JAC (JavaScript & CSS).
Another reason I created this functionality is because I have this ongoing project (ControlLibrary.csproj) that I am plugging these into. When I am done I will release in some form or another whether it be a .DLL or the actual source ($1 Million USD ;-) ). This way you don't have to download and install the ASP.NET AJAX Control Library to access functionality that really isn't even Ajax (in some cases like, not even close).
The watermarked textbox is a perfect example of something that is easy to recreate with elementary JavaScript and CSS. First the CSS we will use.
<
style type="text/css">
.watermark
{
background-image: url('images/overlay.gif');
background-repeat: no-repeat;
padding-left: 20px;
vertical-align: middle;
color: DarkGray;
}
.normal
{
}
</
style>
Even with the ASP.NET AJAX Library you need to specify the CSS classes to use. Here I have the "watermarked" and "unwatermarked" CSS class (normal). Watermark gives our textbox a little overlay image and changes the font-color to a darker shade of gray similar to the ASP.NET AJAX version. For the overlay.gif I just resized an icon with Paint.NET. FYI, a default ASP.NET TextBox control is roughly 21px high. In this case, I resized to 19px high and set the vertical-align to middle. Normal has no style and gives us a normal-looking textbox. Easy.
Next we need to add some client-side functionality to our textboxes. I simply put this in the Page_Load Event and when I create an actual control for this in the future this will be done behind the scenes based on properties that are set by the end-user. But, for proof-of-concept here's what I did:
string defaultValue = "Enter Value";
textFirstName.Attributes.Add("class", IsPostBack ? "normal" : "watermark");
textLastName.Attributes.Add("class", IsPostBack ? "normal" : "watermark");
if (!IsPostBack)
foreach
(Control control in Page.Form.Controls)
if (control is TextBox)
{
TextBox textbox = (TextBox) control;
textbox.Attributes.Add("onfocus", string.Format("OnFocus('{0}', '{1}')", textbox.ClientID, defaultValue));
textbox.Attributes.Add("onblur", string.Format("OnBlur('{0}', '{1}')", textbox.ClientID, defaultValue));
textbox.Text = defaultValue;
}
Simple and painless. Setting attributes for TextBox to call our client-side functions. Again, this will be done behind-the-scenes from the control I am creating. The JavaScript is pretty basic also.
<
script type="text/javascript" language="javascript">
function
OnFocus(elementId, defaultText)
{
if (document.getElementById(elementId).value == defaultText)
{
document.getElementById(elementId).className = "normal";
document.getElementById(elementId).value = "";
}
}
function
OnBlur(elementId, defaultText)
{
var textValue = document.getElementById(elementId).value;
if (textValue == defaultText || textValue.length == 0)
{
document.getElementById(elementId).className = "watermark";
document.getElementById(elementId).value = defaultText;
}
else
document.getElementById(elementId).className = "normal";
}
</
script>
All we have to worry about are 2 relatively simple methods to handle the onfocus (or could've been onclick) and onblur event (or could've been ontextchanged). Since we are passing in the default value and the textbox's id we can compare the values and set the CSS class appropriately.
Here's what the finished product looks like.

You can see how this would be very easy to encapsulate into a control. When I am done you will be able to add to your form like this:
<
wa:WatermarkTextBox WatermarkCss="a" NormalCss="b" Text="asdf" />
Easy.
Tags:
C#,
JavaScript
I apologize for not posting this sooner. If it weren't for a very loyal (and patient!) visitor I would've probably forgot about this.
Long story short: I have done about 20 clean installs of Windows Vista Ultimate over the last 3 weeks and pretty much didn't bother transferring all of my old files in the case that I would have to reinstall Ultimate again. It's been 3 weeks without a hitch so I figure now is as great of a time as ever.
So, for those of you that are still interested you can download the "ActualReorderList" project. It's a Visual Studio 2005 (C#) project. The data comes from a ProdutGroup class with candy-coded data (i.e. not coming from a database). I plan on adding this functionality in the future but for now just want to get the point across.
Let me know what you think.
@ Chetan: Thanks for the reminder :-)
Related Links
An Actual ReorderList Control with Substance Part 1
An Actual ReorderList Control with Substance Part 2
Tags:
Code,
JavaScript
I find it odd that the ASP:TextBox doesn't pay attention to MaxLength attribute when your textbox is multiline. I created some very simple utility functions to help with this. Some of you might find it useful.
<strong>Reason:</strong> <asp:RequiredFieldValidator ID="requiredReason" ControlToValidate="textReason" ErrorMessage="*" runat="server" /><br />
<asp:TextBox ID="textReason" TextMode="multiline" Height="70" Width="200" MaxLength="200" runat="server" onkeyup="displayLengthCount(textReason', 'displayLengthCount')" onchange="restrictLength(200, textReason', submitRequestTimeOff')" /><br />
<label id="displayLengthCount">0</label> of 200 available
<p><asp:Button ID="submitRequestTimeOff" Text="Request Selected Date" runat="server" OnClick="SubmitClick" /></p>
This is an example of what I would have in my .aspx page. Very simple example of having to type in a reason for requesting time off. Notice how I have the 200 as my MaxLength. You don't even need that as it will be disregarded when the rubber meets the road.
Here is the javascript that I am using. Two very simple methods.
function
restrictLength(length, elementIdText, elementIdSubmit)
{
if (document.getElementById(elementIdText).value.length > length)
{
alert("This field has a maximum length of " + length + ". Please truncate");
document.getElementById(elementIdSubmit).disabled = true;
}
else
document.getElementById(elementIdSubmit).disabled = false;
}
function
displayLengthCount(elementIdText, elementIdDisplay)
{
document.getElementById(elementIdDisplay).innerHTML = document.getElementById(elementIdText).value.length;
}
The first utility function (restrictLength) takes the length (what you deem as MaxLength), the textbox / control to validate, and the button that submits the form (to enable / disable accordingly). In my example I call this on the onchange event but you can just as easily switch to the onblur as they are both pretty similar. For those of you that don't know, the onchange fires when the contents of the textbox has changed. The onblur event fires when the object loses input focus. I say they are both similar because the onchange events action of analyzing the input is initiated after changing the selection aka losing input focus.
After you type your text the method is called and the input is analyzed. If the input is valid, nothing happens! If the input is invalid, the user gets an alertbox telling them so and the button that submits that form data is disabled and will remain disabled until the data is truncated.
The second and last utility function simply gives the end-user a visual representation of how many characters they have left in the current input field. Very easy to understand and I don't know why I didn't think of this sooner.
I am going to insert an animated demo here as soon as I figure out why it won't record. Until then, have fun with this.
Also, I probably should've disabled the submit button as soon as the input length reached 201 but I figure for fun I should allow the user to type a long-winded novel only to have to backspace a lot or select-all, delete, and start all over.
UPDATE
Tags:
JavaScript

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
Tags:
AJAX,
C#,
JavaScript
Just as the title says. I can't even begin to tell you how much I absolutely hate the Ajax Toolkit Reorder List control. I've blogged about this many times. It sucks. Big time. I've told many colleagues (real-life and internet-life) the story about how I got tired of trying to strong-arm the Reorder List control and after fruitless efforts I decided to learn a little JavaScript and roll-my-own functionality.
I figure it would be fun to prove this and share some code with everyone. The task is very involved so I will publish in installments. When all is said-and-done I will give away the code along with a sample project and you can all do what you will. Again, since I've been extremely busy both at work and at play this will take some time but should be fun nonetheless.
So. The example will focus on reordering products within product groups. Each product group has products attributed to it. It's not rocket science. This is also a very real possibility and something that could benefit from having a drag-and-drop interface. Our project will use the following 2 objects: Product and ProductGroup.
Product
public class Product
{
private int _productId = 0;
private string _productName = string.Empty;
private decimal _productPrice = 0m;
public int ProductId
{
get { return _productId; }
set { _productId = value; }
}
public string ProductName
{
get { return _productName; }
set { _productName = value; }|
}
public decimal ProductPrice
{
get { return _productPrice; }
set { _productPrice = value; }
}
}
ProductGroup
using System.Collections.Generic;
public class ProductGroup
{
private int _productGroupId = 0;
private string _productGroupName = string.Empty;
private List<Product> _products = new List<Product>();
public int ProductGroupId
{
get { return _productGroupId; }
set { _productGroupId = value; }
}
public string ProductGroupName
{
get { return _productGroupName; }
set { _productGroupName = value; }
}
public List<Product> Products
{
get { return _products; }
set { _products = value; }
}
}
Pretty straight-forward. For this project I will use one Default.aspx and a javascript file which I won't publish all the code for right now as it won't make sense. What I will do is give some snippets and show an animated demo of the code in action, then spend some time in the future dissecting it / answering questions that come to me in the form of comments, etc...
So what I like to do with drag-and-drop web applications is create an xml document representing our objects. I like to think of drag and drop applications as a sort of Xml parent-child node issue. If you ever find yourself needing to program functionality that allows the moving of nodes then drag-and-drop might be a viable option. For this first installment I am simply creating an xml document and binding it to an Xml object that we can reference using the JavaScript DOM (aka XPath queries!). For the following demo I am using this Xml format:
<?
xml version="1.0" encoding="utf-8"?>
<root>
<productGroup productGroupId="1">
<product id="1" name="Product-1" price="1" />
<product id="2" name="Product-2" price="2" />
<product id="3" name="Product-3" price="3" />
<product id="4" name="Product-4" price="4" />
</productGroup>
<productGroup productGroupId="2">
<product id="5" name="Product-5" price="5" />
<product id="6" name="Product-6" price="6" />
<product id="7" name="Product-7" price="7" />
<product id="8" name="Product-8" price="8" />
</productGroup>
<productGroup productGroupId="3">
<product id="9" name="Product-9" price="9" />
<product id="10" name="Product-10" price="10" />
<product id="11" name="Product-11" price="11" />
<product id="12" name="Product-12" price="12" />
</productGroup>
</root>
This file is created with this code:
private void CreateXmlFile(string xmlFilePath)
{
int productGroupId = 1;
using (XmlTextWriter xml = new XmlTextWriter(Server.MapPath(xmlFilePath), new UTF8Encoding(false)))
{
xml.Formatting = Formatting.Indented;
xml.Indentation = 5;
xml.WriteStartDocument();
xml.WriteStartElement("root"); // <root>
ProductGroup.RetrieveAll().ForEach(
delegate(ProductGroup productGroup)
{
xml.WriteStartElement("productGroup"); // <productGroup>
xml.WriteAttributeString("productGroupId", productGroupId.ToString());
productGroup.Products.ForEach(
delegate(Product product)
{
xml.WriteStartElement("product"); // <product>
xml.WriteAttributeString("id", product.ProductId.ToString());
xml.WriteAttributeString("name", product.ProductName);
xml.WriteAttributeString("price", product.ProductPrice.ToString());
xml.WriteEndElement(); // </product>
});
xml.WriteEndElement(); // </productGroup>
++productGroupId;
});
xml.WriteEndElement(); // </root>
}
xmlData.Attributes.Add("src", xmlFilePath);
}
You see that I add xmlData src attribute to this file? That is simply an xml object in my .aspx that looks like this:
<
xml id="xmlData" runat="server" />
Before I get an influx of inquiries asking why I just didn't use an ASP:XmlDataSource control I don't like the xml being displayed in my source. I'm a little picky and a little old school at the same time.
I'm going to skip all the JavaScript explanation because I just got done splitting and stacking over a cord and a half of firewood after working 60+ hours this week. The JavaScript is a whole other post in and of itself.
Demo
A little preface. I have 3 rows of 4 products. A row is a product group and a textbox is representing a product. The numbers in the textbox represent the product price (I know, cheesy) and the label with the $ represent the total of the numbers in the textbox. Again, this is only to pique your interest. If you are interested then stay tuned. If not, then still stay tuned and let me know how much of an idiot I am for not being able to do this with the AJAX Toolkit. One major advantage to rolling your own is that instead of dragging the ENTIRE object up one row, you can pick and choose and have a TRUE drag-and-drop interface. When I first rolled this out to production I added empty textboxes that could be used as well. This way one parent could have 5 children, one could have 1 and another could have 0 (I had functionality to delete nodes as well).
I tried to go pretty slow and only showed 3 moves to give you time to count and verify that this is actually working. Again, I haven't showed yet how I'm doing the javascript. Right now the javascript file is about 75 lines or so. In the end, depending on how ambitious I am, we'll be at about 200 or so as I'd like to give you all the ability to swap parent nodes as well as child nodes. The JavaScript might be a little heavy but in my opinion, it is a small price to pay to get the ACTUAL functionality that you want! Animated demo below.

Easy. Stay tuned for HOW I did that.
Related
An Actual Reorder List Control with Substance Part 2
Tags:
AJAX,
JavaScript
I've been doing a ton of AJAX / JavaScript work lately on a new and exciting project. One aspect of this project that came up today was a mailing subscription function. This is in the sidebar and is inherited from a masterpage. The thing is, since the master page has 2 content panels I am limited to only one form tag as I now do not place this tag in the masterpage for reasons I won't get into right now. The easiest way to do this is of course create some AJAX function that calls a Generic Handler (new favorite) that adds a record into the database. This would work great but since I didn't create the UI and had to think of a very quick (emphasis on very) solution I decided to add an onclick event the subscribe image button that would pop-up a window (subscribe.aspx) and have a querystring value for the email address. The pop-up page would attempt to add the email address if it didn't already exist and then close itself. If the email address already existed then a message could be displayed to the user and they can manually click on a link to close the window. For the basis of this entry I'll do the bare-bones and just assume you add every email address to the database.
protected
void Page_Preinit(object sender, EventArgs e)
{
if (Request.QueryString["action"] != null)
bodyTag.Attributes.Add("onload", "javascript:(window.close())");
}
protected void Page_Load(object sender, EventArgs e)
{
string emailAddress = Request.QueryString["emailAddress"];
if (!string.IsNullOrEmpty(emailAddress))
Subscription.Subscribe(new Subscription(emailAddress));
Response.Redirect("subscribe.aspx?action=close");
}
Lately I've started to add functionality in the Page.PreInit event. PreInit occurs at the begin of the page initialization process (duh) and will allow you to do things such as apply personalization information, themes, or in this case client-side javascript before the Page.Load.
For this to work I had add the runat="server" attribute to the body tag. I could've added this functionality to the Page.Load event but wanted to separate functionality. Since the site could eventually allow users to apply their own themes I wanted to have the pages "prepped". Do whatever you feel though as it will work from both.
Update
I have since changed this functionality to display a message to the user, timeout for 5 seconds and then close. There is also a link that allows them to close the window earlier if they wish.
Tags:
C#,
JavaScript