Preamble
Over the long weekend I thought a lot about where I wanted to go with the grid "control" for ASP.NET MVC. One of the things that weighed heavily on my mind was the ability to have the control fully customizable. As I thought about this, I decided that first thing's first: I need to get the functionality working and then worry about the prettiness factor. So for those of you concerned about the customizability - it's coming. For now, I really want to focus on the ability of the grid to get work done.
Code Bloat
As I began to dive again into the previous code I realized that having everything in one extension method was going to be... well ugly. The first order of business was to abstract everything in to a separate DLL. In order to do so, I created a new Grid class that handles all of the grid drawing.
JavaScript
I am partial to using JQuery. So here is the general idea of what the JavaScript code needs to do:
- Detect that the user would like to edit a particular row
- Detect any rows previously being edited (to move the edit focus to the new row being edited)
- Display text boxes for the user to have the ability to edit the values
- Post back an edit or a delete
In order to do each of the items above, I needed to change what the grid was outputting a little. First I needed to wrap the table in a <form> tag in order to retrieve any valid values being edited. Also, I needed to add the following links: Edit, Delete, Cancel, Save. The first two (Edit, Delete) should be shown when a row was not in edit mode. The second two (Cancel, Save) should only be shown when a row is in edit mode. I also wanted to make sure that only one row was being edited at a time. Here is a fragment of the HTML the control produces:
<form name="Student_Form" id="Student_Form">
<table id="Student" style="border: solid 1px black;width:100%;">
<tr id="Student_6">
<td class="edit">
<span class="editor">
<a id="6_Student_edit" href="#">Edit</a> <a id="6_Student_delete" href="#">Delete</a>
</span>
<span class="editing">
<a id="6_Student_cancel" href="#">Cancel</a> <a id="6_Student_save" href="#">Save</a>
</span>| 6</td>
<td class="FirstName">Frances</td>
<td class="LastName">Adams</td>
<td class="Age"> </td>
<td class="Address"> </td>
<td class="City"> </td>
<td class="State"> </td>
<td class="Zip"> </td>
<td class="Phone"> </td>
</tr>
...
Once I decided on the DOM elements the form would produce, it was time to put some JavaScript to each row. The first bit of code is designed to attach events to each of the links in the grid as well as hide the "editing" spans since each row by default starts in non-edit mode:
$('document').ready(
function(){
$('#Student span.editing').hide();
$('#Student a').click(
function(event){
event.preventDefault();
handleEditClick(this.id);
}
);
}
);
This code ensures that the editing spans are hidden and each click event on the anchor tags are redirected to the handleEditClick function. Notice that I pass in the id of the actual anchor tag. These I defined as id_Grid_action (see above). Whenever a link is pushed I get those three pieces of information to proceed with processing. Now for the handleClick function (it is long):
function handleEditClick(itm) {
var o = itm.split('_');
var id = o[0];
var grid = o[1];
var action = o[2];
var name = '#' + grid + '_' + id + ' td';
if(action == 'edit')
{
// un-edit any others that might be in editmode
$('#Student span.editor:hidden a:first').each(
function() {
var cl = this.id.split('_');
$('#' + cl[0] + '_' + cl[1] + '_cancel').click();
}
);
$('.editor', name).hide();
$('.editing', name).show();
$(name).each(
function() {
if($(this).hasClass('edit')) return;
var data = $(this).text() == ' ' ? '' : $(this).text();
$(this).html('<input type="text" name="' +
$(this).attr('class') +
'" value="' +
data +
'" size="10" />');
}
);
}
else if(action == 'cancel')
{
$('.editor', name).show();
$('.editing', name).hide();
$(name).each(
function() {
if($(this).hasClass('edit')) return;
var data = $('input', this).val();
$(this).html(data == '' ? 'nbsp;' : data);
}
);
}
else if(action == 'save')
{
//alert('Save ' + itm);
var arr = $('#Student_Form').serializeArray();
$.each(arr,
function(i, field) {
alert(field.name + ': ' + field.value);
}
);
}
else if(action == 'delete')
alert('Delete ' + itm);
As advertised, I first break up the id_gird_action pair into variables that will be useful. Also notice that ALL actions come into this function. First lets focus on the edit action. Now this is why I love JQuery:
$('#Student span.editor:hidden a:first').each(...
This particular line of code selects the first anchor tag under a span with class editor that is hidden from the Student grid. Why would I want to do that? From the id of the anchor tag I can reconstruct the id of the cancel anchor tag and then "click" it in order to cancel the update. To do it any other way would be difficult (at least I think so). The elegance of JQuery allows for those kinds of things. Once we cancel any other edit, we proceed to go through each TD in the row in question (with the exception of the edit anchors) and push the data into text boxes. Also, we set the appropriate edit spans to visible and hidden in order to have the correct actions displayed. Next, the Cancel action. The job of the cancel action is to take the data out of the text boxes and stuff them back into the TD tag. I realize that I should probably add a "Do you want to save this?" confirm box, but I will leave that for later. Notice again the elegance of JQuery:
var data = $('input', this).val();
The $(INPUT, TD) functions as a selection within a previous selection. In other words, within the current TD, find me an INPUT HTML element and retrieve the value. Once we retrieve the value, we can put it back into the TD tag without the INPUT element.
The Grid Class
The first thing I did was write the JavaScript code with the output from the Grid class. In other words, I ran the flat table (without the JavaScript) and cut and pasted the table to a standard HTML file. Once I had the new HTML file I worked on the JavaScript until it worked as I expected. Now the only problem left was creating a RenderJavaScript() that emitted the Grid specific JavaScript we needed. Doing that seemed a bit tedious so I downloaded a nifty little Add In that did it for me.
Outcome
Some screens:
Usage
Now that I've abstracted the Grid out to a completely separate project, I changed the helper class to this:
public static string ToAjaxGrid<T, TKey>(this IEnumerable<T> list,
Func<T, TKey> key,
string name)
{
Grid<T, TKey> grid = new Grid(list, key, name);
return grid.Render();
}
This change allowed me to leave the code in the View the same:
<%= ViewData.Model.ToAjaxGrid(s => s.StudentId, "Student") %>
Next Time
I think the client side functionality is (mostly) done. For the next installment I will try to actually submit the requested actions to the MVC controller (save, delete).
Code
Download
Tags:
Categories: Ajax | ASP.NET | JQuery | MVC
Currently rated 5.0 by 1 people - Currently 5/5 Stars.
- 1
- 2
- 3
- 4
- 5
Permalink
E-mail |
Kick it! |
DZone it! |
del.icio.us
Post RSS
Posted on 2. July 2008 15:20 by seth|
At work we are starting a major project and I think we have decided to go the ASP.NET MVC route. One of the concerns we had was that we could not use traditional "web controls" (i.e. Telerik, DevExpress) since the all require the <...runat=server> portion. With that said, I thought I would start to take a look at how I would go about doing it! The first "control" I want to get off the ground is a data bound grid that allows asynchronous saves via JavaScript. We'll see how it goes! Premise The idea is to use an extension method to any IEnumerable<T> object that generates the grid. I also want to be able to use CSS to stylize any aspect of the grid. Additionally, there should be a way to edit (in place) any of the records in the grid and post the results back using AJAX. I think this should be an interesting project! Code For this part I just want to print out the stylized table. Here is the result: public static string ToAjaxGrid<T, TKey>(this IEnumerable<T> list,
Func<T, TKey> key,
string name,
NameValueCollection attributes)
{
string format = " style=\"{0}\"";
StringBuilder sb = new StringBuilder();
// Build outer table div
sb.Append(String.Format("<table id=\"{0}\"{1}>\n",
name,
attributes["TableStyle"] != null
? String.Format(format, attributes["TableStyle"]) : ""));
foreach(T item in list)
{
// Build row div
sb.Append(String.Format("<tr id=\"{0}_{1}\"{2}>\n\t",
name, key(item).ToString(),
attributes["RowStyle"] != null
? String.Format(format, attributes["RowStyle"]) : ""));
foreach(var property in typeof(T).GetProperties())
{
// Build row item span
string span = "<td id=\"{0}_{1}\"{3}>{2} | ";
var itm = property.GetValue(item, new object[] { });
itm = itm != null ? itm : " ";
sb.Append(String.Format(span, key(item), property.Name, itm,
attributes["ItemStyle"] != null
? String.Format(format, attributes["ItemStyle"]) : ""));
}
sb.Append("\n</tr>\n");
}
sb.Append("\n</table>");
return sb.ToString();
}
public static string ToAjaxGrid<T, TKey>(this IEnumerable<T> list,
Func<T, TKey> key,
string name)
{
return list.ToAjaxGrid<T, TKey>(key, name, new NameValueCollection(0));
}
Before scratching your head and calling me names, let's see if I can 'splain what I did. The two methods are the same with the exception of the NameValueCollection. The idea behind the NameValueCollection is to be able to pass in the actual CSS styles for the Table, Row, and Item. Now for the nitty gritty. First the data types passed in generically: ToAjaxGrid<T, TKey>. The T represents the actual entity (or the rows) and the TKey represents the type of the primary key. I figured I would eventually need to know the primary key in later work. The "Func<T, TKey> key" paramter is a lambda that takes the T object and produces a TKey object. When iterating through the rows, it is important to somehow keep track of the primary key (at least I think it is). The name is the DOM identifier for the table that we will use later. So now the looping magic! It uses reflection to infer the properties of type T and then loops through each T in the IEnumerable<T> and then through each property in T. And we are done!
Usage
Here is a snippet of what it looked like when I used it:
<% var col = new NameValueCollection(); %>
<% col.Add("TableStyle", "border: solid 1px #000000; width: 100%"); %>
<% col.Add("ItemStyle", "border: solid 1px #00FF00; spacing: 5px"); %>
<%= ViewData.Model.ToAjaxGrid(s => s.StudentId, "students", col) %>
And what it looked like:
Disclaimer
I have never done this before! If you think there is a smarter way, let me know so I can fix my stuff!
Tags:
Categories: Ajax |
ASP.NET |
MVC
Currently rated 3.3 by 3 people
- Currently 3.333333/5 Stars.
- 1
- 2
- 3
- 4
- 5