Creating Advanced ASP.NET MVC Controls (Part 3, A Scheduler)

by Seth Juarez 18. August 2009 11:16

Purpose

This is part 3 of a series going through the process of creating an advanced control for the ASP.NET MVC system. I've decided to create a schedule control that allows a user to schedule and item on a calendar control as well as add some meta-data information to the scheduled date. Together with the debugger we have built, this should not be too difficult

Getting Started

Whenever I start building a new control, I simply go commando-style and write the html, css, and javascript first to make sure everything looks good on that end. This helps me with debugging.

The Markup

First things first: the html. I like to do calendars this way:

<table class="scheduler_month">
	<tr class="scheduler_month_header">
		<th colspan="7">September 2009</th>
	</tr>
	<tr class="scheduler_days_header">
		<td>Sun</td>
		<td>Mon</td>
		<td>Tue</td>
		<td>Wed</td>
		<td>Thu</td>
		<td>Fri</td>
		<td>Sat</td>
	</tr>
	<tr class="scheduler_month_days">
		<td class="scheduler_month_invalid_day"></td>
		<td class="scheduler_month_invalid_day"></td>
		<td class="scheduler_month_day">1</td>
		<td class="scheduler_month_day">2</td>
		<td class="scheduler_month_day">3</td>
		<td class="scheduler_month_day">4</td>
		<td class="scheduler_month_day">5</td>
	</tr>
	<tr class="scheduler_month_days">
		<td class="scheduler_month_day">6</td>
		<td class="scheduler_month_day">7</td>
		<td class="scheduler_month_day">8</td>
		<td class="scheduler_month_day">9</td>
		<td class="scheduler_month_day">10</td>
		<td class="scheduler_month_day">11</td>
		<td class="scheduler_month_day">12</td>
	</tr>
	<tr class="scheduler_month_days">
		<td class="scheduler_month_day">13</td>
		<td class="scheduler_month_day">14</td>
		<td class="scheduler_month_day">15</td>
		<td class="scheduler_month_day">16</td>
		<td class="scheduler_month_day">17</td>
		<td class="scheduler_month_day">18</td>
		<td class="scheduler_month_day">19</td>
	</tr>
	<tr class="scheduler_month_days">
		<td class="scheduler_month_day">20</td>
		<td class="scheduler_month_day">21</td>
		<td class="scheduler_month_day">22</td>
		<td class="scheduler_month_day">23</td>
		<td class="scheduler_month_day">24</td>
		<td class="scheduler_month_day">25</td>
		<td class="scheduler_month_day">26</td>
	</tr>
	<tr class="scheduler_month_days">
		<td class="scheduler_month_day">27</td>
		<td class="scheduler_month_day">28</td>
		<td class="scheduler_month_day">29</td>
		<td class="scheduler_month_day">30</td>
		<td class="scheduler_month_invalid_day"></td>
		<td class="scheduler_month_invalid_day"></td>
		<td class="scheduler_month_invalid_day"></td>
	</tr>
</table>
This comes out looking like:

1stPassCalendar

Styles...

It looks pretty good as a starting point. With a little css we can actually make it look good (Disclaimer: I write programs and thus cannot be trusted with what "looks good"):

.scheduler_month
{
	border: solid 1px #C0C0C0;
	border-collapse: collapse;
}

.scheduler_month_header th
{
	background: #714546;
	color: white;
	height: 20px;
	width: 210px;
	font-size: 12px;
}

.scheduler_days_header td
{
	background: #FFA54C;
	color: white;
	height: 20px;
	width: 30px;
	font-size: 12px;
	text-align: center;
	font-weight: bold;
}

.scheduler_month_invalid_day, .scheduler_month_day
{
	height: 20px;
	width: 30px;
	font-size: 12px;
	text-align: center;
	border: solid 1px #C0C0C0;
}

.scheduler_month_invalid_day
{
	border: none;
	background: #E2E2E2;
}

Here is the outcome:

2stPassCalendar

Functionality?

The goal of the control is to save/edit data for each day and mark the calendar if there is any associated data with the day. In order to do this, we need to have a mini-form to take and display data:

<div id="DayData">
	<div class="label">Date:</div>
	<div id="DayCurrent" class="input"></div>
	<div class="label">Title:</div>
	<div class="input"><input type="text" id="DayTitle" name="DayTitle"/></div>
	<div class="label">Description:</div>
	<div class="input"><textarea id="DayDescription" name="DayDescription"></textarea></div>
	<div class="button">
		<input type="button" value="Cancel" id="DayCancel" name="DayCancel" />
		<input type="button" value="Save" id="DaySave" name="DaySave" />
		<input type="hidden" id="DayId" name="DayId" />
	</div>
</div>

This little number has the visible textboxes where the interaction takes place as well as a hidden field that will allow us to maintain state (DayId). Adding some more styles we end up with:

3rdPassCalendar

Do some work already!

Now for some jQuery magic! We want to display the mini-form, gather data, and persist it (at least on the client side for now). Some JavaScript first:

$('.scheduler_month_day').click(function(event) {
	var id = this.id;

	// proper date object
	var d = convertDate(id);
	
	// put in value (if exists)
	var index = window.Changes.find(function(x) { return x.Id == id; });
	if(index > -1) {
		$('#DayTitle').val(window.Changes[index].Title);
		$('#DayDescription').val(window.Changes[index].Description);
	} else {
		$('#DayTitle').val('');
		$('#DayDescription').val('');
	}

	// set the id to proper cell reference
	$('#DayId').val(this.id);
	$('#DayCurrent').text(d.toDateString());

	// make it look nice when we show it
	if(!$('#DayData').is(':hidden'))
		$('#DayData').fadeOut('fast').hide();

	$('#DayData')
		.css({left: event.clientX + 10, top: event.clientY + 10})
		.fadeIn('slow').show();
});


window.Changes = new Array();
$('#DaySave').click(function() {
	// get values
	var id = $('#DayId').val();
	var title = $('#DayTitle').val();
	var desc = $('#DayDescription').val();
	
	// already in there?
	var index = window.Changes.find(function(x) { return x.Id == id; });

	// do appropriate thing if it already exists
	if(index == -1)
		window.Changes.push({ Id: id, Title: title, Description: desc });
	else
		window.Changes[index] = { Id: id, Title: title, Description: desc };

	$('#' + id).addClass('scheduler_month_day_data');

	// close win
	$('#DayData').fadeOut('fast').hide();

	// make sure everything is ok
	if(window.isDebug)
		_(window.Changes).clear()
			.write('Current Change Set');
});

Explanation

Note that the crux of the code does the following:

  1. Fill in form data (if it exists)
  2. Save or update data (depending on whether or not it exists in the first place)
The magic is on line 31 where there is a global array that maintains all of the changes. This is where the debugger comes in handy. It allows us to visualize what data has been persisted on the client side. On line 8 and 39 there is an interesting function worthy of mentioning. What I have done is "extend" the functionality of the Array object by adding:

Array.prototype.find = function(x) {
    for (var i = 0; i < this.length; i++) {
        if (typeof (x) == 'function' && x(this[i]))
            return i;
        if (this[i] == x)
            return i;
    }
    return -1;
};

The primary job is to figure out if the array has an element in it or not. Either a value or a function can be passed in. Naturally I chose the function parameter since we are dealing with an array of objects. What it does is tell me if the save is an update or an addition. Looking the the control code again, we can see this happening on lines 9 and 42. It decides what to do if the item does not exist by using the DayId (line 18) that we save and later retrieve. Here is a picture of the whole thing (debugger and all):

finalPassCalendar

Where to from here?

So now what? This control is all nice and all, but here are some things left to do:

  1. Make the thing more generic (i.e. it needs to be able to represent any month or number of months)
  2. Get initial data from persistent storage (i.e. auto populate values)
  3. Save changes to persistent storage

A word about the debugger...

I have made some minor changes to the debugger. They revolve mainly around usability. It is also important to note that if you try to visualize deep objects using the debugger, you will stall your browser. It is designed for small-ish things (remember it was like a 2-4 hour thing I made) so if it hangs on you, you've been warned

Code Already

I would love feedback on the code. Are there any ommissions/improvements/rants/raves/etc.? Hope this has been helpful!

Tags: , , , ,

Ajax | ASP.NET | JQuery | MVC

Creating Advanced ASP.NET MVC Controls (Part 2, Finished Debugger)

by Seth Juarez 17. August 2009 16:14

Purpose

As mentioned in the previous post, in order to create good client side controls that interact well with the ASP.NET MVC system, we need to have a way to visualize data that the control either generates, or passes to the controllers. I found this difficult to acheive in IE as well as Firefox. I did not need/want all of the complexities of Firebug or IE's Developer tools (which are great btw). I just wanted to see my data! The problem with the previous version of the debugger was that it was too dang simple! If I were to use it like this:

_('Test').clear()
	.write('(Simple)')
	.set(complexArray)
	.write('(Complex Array)')
	.set({ one: { a: 'Hospital', b: 4, c: {x:3,y:5}}, two: 'test'})
	.write('(Complex Object)')
	.set(function(arg) { alert('HI!' + arg); })
	.write('(function)')
	.set({ one: new Array('Happy', 'debugger', 'array'), two: 'test'})
	.write('(Complex Object)')
	.set(
		new Array(
			new Array(
				{a: 1, b: 2, c: 3},
				{a: 4, b: 5, c: 6},
				{a: 7, b: 8, c: 9}), 
			new Array(
				{a: 'one', b: 'two', c: 'three'}, 
				{a: 'four', b: 'five', c: 'six'}, 
				{a: 'seven', b: 'eight', c: 'nine'})))
	.write('(Complex Array)');

});

the debugger would simply not be good enough!

On to version 2!

In order to actually print something useful given the complexity of the potential objects, we need to use some recursion. The bottom level of the recursion would simply be printing out simple data types:

  1. either a function (yes this is a data type),
  2. a string, or
  3. a number

The debugger would then simply recurse over arrays and objects down to the base types. This turned out to be harder because of a simple mistake I made. What is the difference bewteen these two snippets:

Snippet 1

writeArray: function(li, arg) {
	for(var i = 0; i < arg.length; i++) {
		$(li).append('[' + i + ']:')
		this.dispatcher(li, arg[i], true);
		$(li).append('<br/>');
	}
}

Snippet 2

writeArray: function(li, arg) {
	for(i = 0; i < arg.length; i++) {
		$(li).append('[' + i + ']:')
		this.dispatcher(li, arg[i], true);
		$(li).append('<br/>');
	}
}

Nothing you say? Nay dear reader, it turns out that snippet 2 is VERY wrong! During recursive calls, the variable i if not re-declared using the var keyword gets reassigned in each recursive call to itself: this leads to a short-circuiting of the for loop.

Final Product

In this version I removed the jQuery UI references because the draggable got annoying. Here are some screenshots:

screen1

and

screen2

Final Words

In the next post, we will actually start to build a control. I am thinking some kind of advanced date-picker thing-y (that is a technical term) that allows users to toggle multiple dates and add meta information to said dates. Here is the code

.

Tags: , , , ,

Ajax | ASP.NET | JQuery | MVC

JQuery MVC Form Helper

by Seth Juarez 30. July 2008 16:53

Simple Form Helper

As I continue to use JQuery and MVC I am completely impressed with how much you can do with very little. I know the new preview 4 came out with an AjaxForm helper. I could not resist, so I made my own and added it to the whole controls project I've been working on. (I have not forgot about the grid, but I've gotten a little bored of it for the time being although I do want to finish it...). So here is the idea:

  1. Have complete control over HOW the form is rendered
  2. Be VERY light (I attached the whole thing to the onSubmit attribute on the form)
  3. Make it fast (it took me like a half an hour or so)

Using it

I found this nifty way of using an action delegate that "reads between the lines." The motivation came from the MVCContrib grids ability to completely define inline ASP.NET looking code and pass it into the helper. Here is how you use it (for some reason I find it useful to see how it works first, and then explain how it works):

<% Html.JQueryForm("studentForm",   
      c => c.EditStudent(),   
      (ViewData.Model as IEnumerable).First(),    
      s =>    
      { %>   
          <%= Html.TextBox("Id", s.Id.ToString()) %>   
          <%= Html.TextBox("FirstName", s.FirstName) %>   
          <%= Html.TextBox("LastName", s.LastName) %>   
          <%= Html.SubmitButton("Submit", "Submit") %>  
    <% }  
   );   
%>

Parameter explanation:

  1. The name to give the form
  2. The action that should be invoked on the controller
  3. The POCO object that holds the data
  4. What to render (the Action delegate that says take a student S and make Html textboxes)

The types passed in are the POCO data object and the controller that will get the action.

Building it

I really just copied a lot of what I have already done with the grid (see previous posts) and made it more lightweight. First the helper:

public static void JQueryForm<T, TController>(this HtmlHelper helper, 
	string name, Expression<Action<TController>> editAction, 
	T data, 
	Action<T> block) 
	where T : class
	where TController : Controller
{
	Form f = new Form(name, helper.BuildUrlFromExpression<TController>(editAction), 
		helper.ViewContext.HttpContext);
	f.RenderOpen();
	block.Invoke(data);
	f.RenderClose();
}

Notice how simple it really is! Using the helper, I build a url from the controller expression, and then pass everything else into a Form object that only does 2 things: render the opening form tag, and render the closing form tag. The only catch is rendering the correct JavaScript code in the onSubmit attribute of the form tag. The rest was simple. Notice on line 4 above the delegate that takes the markup. This markup is rendered on line 11. For the "hard" part, I just copied the JQuery Ajax call from the Grid Control I have been working on. Here is the gist of it:

var formData = $(this).serializeArray();
$.ajax({
     type : 'POST',
     contentType : 'application/x-www-form-urlencoded',
     url : 'edit ACTION here',
     data : formData,
     dataType : 'json',
     success : function(msg){   
         alert('Complete! ('+msg+')');
     }
});
return false;

The return false is there to prevent the form from posting back.

Screenshot

Here are some screenshots of the whole thing:

The form rendered:

image

The postback:

image

Notice that I used the Request helper I built in a previous post. In order to do this, I am required to name the textboxes with the same name as the property of the object. This is how the helper resolves that appropriate attribute.

Code

Here is the code. You will find fragments of me starting to do the grid code as well.

Tags: , ,

Ajax | JQuery | MVC

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

Pages

RecentComments

Comment RSS