Ayende @ Rahien

It's a girl

Cascading Drop Downs in MonoRail

I just had this need, and I decided to explore what I could do without referring to what other people has already done. There are probably better ways, but this one has tickled my fancy.

Anyway, the idea here is to display three levels of hierarchy in a profression. Here is the HTML:

<table>
	<tr>
		<td>
			${res.PrimaryProfession}
		</td>
		<td>
			${Form.Select("primaries",PrimaryProfessions, 
{@value: @Id, @text: @Name, @firstoption: res.Select}) } </td> <td> ${res.SecondaryProfession} </td> <td> ${Form.Select("secondaries",SecondaryProfessions,
{@value: @Id, @text: @Name, @firstoption: res.Select}) } </td> <td> ${res.TertiaryProfession} </td> <td> ${Form.Select("tertiaries",TertiaryProfessions,
{@value: @Id, @text: @Name, @firstoption: res.Select}) } </td> </tr> </table>

This is not very fancy, and the CSS guys would be all over me for using a table, but let us leave that aside. Basically, we render three select elements. Now, let us see the server side, shall we?

public void Index()
{
	PropertyBag["PrimaryProfessions"] =
		professionRepository.FindAllPrimaries();

	PropertyBag["SecondaryProfessions"] =
		professionRepository.FindAllSecondaries();

	PropertyBag["TertiaryProfessions"] =
		professionRepository.FindAllTertiaries();
}

The ProfressionRepository uses Repository<T> internally, I want to avoid using Repository<T> directly in my controllers. Now, we need to actually respond to change events on the client side:

$j(document).ready(function()
{
	$j('#primaries').change(function()
	{
		$j.ajax({ 
			url:'GetSecondaryProfressionsByPrimary.ashx',
			data: { id: $j('#primaries').val() },
			dataType: 'script'
		});
	});
	hookSecondariesChangeEvent();
});

function hookSecondariesChangeEvent()
{
	$j('#secondaries').change(function()
	{
		$j.ajax({ 
			url:'GetTertiaryProfressionsBySecondary.ashx',
			data: { id: $j('#secondaries').val() },
			dataType: 'script'
		});
	});
}

This is done using jQuery, to hook the events and issue Ajax requests to the server when they are done. Note that I am registering the seconderies change event in a separate function, it will be soon clear why.

Now, how does the server handles this?

public void GetSecondaryProfressionsByPrimary(Guid id)
{
	PropertyBag["SecondaryProfessions"] =
		professionRepository.FindSecondariesByPrimaryIdOrAll(id);
}

public void GetTertiaryProfressionsBySecondary(Guid id)
{
	PropertyBag["TertiaryProfessions"] =
		professionRepository.FindTertiariesBySecondaryIdOrAll(id);
}

So we just pass the parameters to the repository, which does all the work for us, in this case, getting the children if the id is valid, or all if it isn't. Now let us move to the relevant views, shall we? We are using BrailJS here, so here is GetSecondaryProfressionsByPrimary.brailjs:

page.Replace( 'secondaries', 
	Form.Select('secondaries',SecondaryProfessions, 
		{@value: @Id, @text: @Name, @firstoption: res.Select})			
)
page.Call( @hookSecondariesChangeEvent );

As you can see, I ask the page to replace the element secondaries with the newly rendered select element. I then acll the hookSecondariesChangeEvent function, since I actually replace the entire element, and not just its content. I am doing this because I don't feel like extracting the part that renders just the options from the Form.Select() method, but I will probably will the next time I need it.

The GetTertiaryProfressionsBySecondary.brailjs is actually simpler:

page.Replace( 'tertiaries', 
	Form.Select('tertiaries',TertiaryProfessions, 
		{@value: @Id, @text: @Name, @firstoption: res.Select})			
)

Again, there are probably betters way, but this is the one I just came up with.

Comments

kevin
10/08/2007 12:20 PM by
kevin

nothing wrong with using tables...people gt all up in arms about that sometimes. tables have their place.

also, nice to see you using JQuery! More and more .net articles/posts seem to referencec jquery versus asp.net ajax. And of course, monorails just wouldn't jive with asp.net ajax, so JQuery makes sense.

kudos!

Ken Egozi
10/08/2007 01:05 PM by
Ken Egozi

My 3 cents:

  1. Replacing the select tag as a whole is better, since replacing it's innerHTML won't always work, so your alternative is to manually create Option objects and add them to the select's .options array

  2. OT, why would you create IRepo wrappers to hide it from the controller? don't want the web project to be dependant on Rhino.Commons? or any other reason?

  3. I'd have done that a bot different:

have a subview for each of the cascaded elements, have the controller render those views (rather than the jsviews), and use tha javascript to replace the select elements.

that way I gain:

a. avoiding duplication. U use the FormHelper.Select twice, once in the initial view, and again in the brailjs files

b. side effect - keeping the view manipulation code and the call to the server in the same place

Ayende Rahien
10/08/2007 01:47 PM by
Ayende Rahien

2/ because profession is an aggregation of several types, and I don't want to start passing my controller 5 different IRepository

3/ I agree, but for a single line, it is hardly worth it.

Ken Egozi
10/08/2007 01:56 PM by
Ken Egozi

but that's the thing.

afaik, you cannot use subviews with brailjs.

It would also present problems if the subview has newlines, the javascript would creep out unless you'll introduce something like SingleLineViewFilter to remove all newlines from the subview output.

So I'm back to the .xJs being over-complex and an unneeded abstraction to the developer's needs, and causes side-problems like that one.

Ahh. Good ol' debate ...

Will Sahatdjian
10/08/2007 05:53 PM by
Will Sahatdjian

BrailJS to Javascript seems like WebForms to MonoRail. Why does everyone hate writing javascript so much?

By the way, if you like jQuery, I highly recommend Mootools (mootools.net). I've not found a more expressive JS API anywhere.

Oh, and the main argument against tables is reduced maintainability, but their weak semantic value is becoming a good reason not to use them as well. Especially on public-facing, SEO-conscious sites.

Ayende Rahien
10/08/2007 06:46 PM by
Ayende Rahien

Will,

Hm, no.

Those are silly examples, where POJ will more than do, but brailjs is great when you have logic that need to run as JS.

For instance, if the user is an important one, highlight the new order. Which is something that I don't want to decide in JS.

kevin
10/08/2007 07:32 PM by
kevin

@will, build be a layout using css that needs to display tabular data, ala excel, using non-table html, and I don't want to see any IE version and then a FF version, etc.

Too often devs spend time trying to figure out how to display data using some css magic, having to worry about browser versions, etc when the table version looks just as good and is less work/time.

That's all I'm saying

And mootools is nice as well. jquery is just a personal preference. I find it nicer ;P

Ken Egozi
10/08/2007 09:52 PM by
Ken Egozi

@Will: I think I might quote you on that.

@Ayende:

That's what the scripting language (velocity, brail, c#) is for.

You can do those 'decisions' in a normal view, and let the javascript deal with replacing the innerHTML.

I would also rather not to eval on any async call. It feels more secure imho. Having the server returning plain old markup (POM?) is much better.

Ayende Rahien
10/08/2007 10:11 PM by
Ayende Rahien

Ken,

Translate this to your method, please?

page.ReplaceHtml("primaries", { @partial: '/subviews/grid.brail' })

if isImportantCustomer:

page.VisualEffect( @Highlight, "primaries" )

Ken Egozi
10/08/2007 10:29 PM by
Ken Egozi

Ok, I'm sketching here, using prototype syntax as that what I can do in a textarea without any reference.

I surly would have a js function like:

function highlightImportantCustomers() {

$('.importantCustomer').each(function (importantCustomer) {

  SomeCoolEffectLib.HighLight(importantCustomer);

});

}

the grid view would call a oneCustomer subview in a for loop.

the oneCustomer subview would contain:

<%=view.Customer.IsImportant? "class='importantCustomer'" %>

now the same oneCustomer view would get rendered on first page load (as subview), and on async call (just that customer element)

the calling js would do something like:

new Ajax.Updater(actionUrl, {

 parameters: 'customerId=' + this.value,

 onSuccess: highlightImportantCustomers

});

What I gained?

  1. the highlight stuff is at a single place, conviniently in a js file. I can replace SomeCoolLib with another, changing only that line in the js.

  2. The rendering logic for a customer, whether it's as part of the grid, or as a single element on an async call, is the same, located in a single place.

  3. And I didn't need to introduce yet another language to my solution, and yet another abstraction that would've made me loose stuff instead of gain stuff

Ayende Rahien
10/08/2007 10:37 PM by
Ayende Rahien

Where does the decision for the isImportantCustomer goes?

Ayende Rahien
10/08/2007 10:38 PM by
Ayende Rahien

Oh, sorry, missed the part of the view.

Ken Egozi
10/08/2007 10:41 PM by
Ken Egozi

on OneCustomer view:

<div <%=view.Customer.IsImportant? "class='importantCustomer'" %> >

some markup for a customer, blah blah <%=view.Customer.Name %> blah blah

Dave Newman
10/09/2007 11:19 AM by
Dave Newman

Just on a side note, i've never understood why people want ajaxed cascading dropdowns. Surely there's not that many options that it wouldn't be worth pulling all the data in the first request and doing it all client side

Ayende Rahien
10/09/2007 01:30 PM by
Ayende Rahien

Dave,

The reason is two fold:

A/ It is MUCH easier to do these sort of filtering on the server side.

B/ There are lists with many items. Imagine that I have 10 primaries, 10 secondaries per primary and 5 tertiaries per secondary. that gives me quite a few that I need to load

Comments have been closed on this topic.