Ayende @ Rahien

Unnatural acts on source code

ASP.Net Ajax, Error Handling and WTF


I am facing some really unpleasant choices at the moment. And I thought it would be so easy.
I wanted to add global error handling to all the web services that we expose as [ScriptService].
For some reason, they didn't show up in the Application_Error event, and that caused the exception details to be sent to the client.
It looks like for some strange reasons, web services exceptions don't go through the Application_Error, so I implemented a SoapExtension to handle that, but ScriptService doesn't go through the Soap layers, so it obviously doesn't work.
Then I went and looked at the code.
The whole thing is _hard coded_ inside, and there is no way in.
No way to globally capture exception being raised from script services.
No way in to add this ability.
Urgh! Argh!

I mean, it is not like I want something very special, it sounds to me like a reasonable request.
Yes, I know you can turn off the error in the config, but I would like, you know, to log this.

Of course, everything is internal in there, so I can't just go and override the RestHandler. ExecuteWebServiceCall(), I would have to have a private build of this stuff.
Just to be able to log exceptions.

Javascript, lexical scopes and what your momma thought you about variables

Let us assume that we have this amazing javascript function:

function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
		if( nums[i] % 2 == 0)
		{
			alertLink.onclick = function() { alert('EVEN: '+ nums[i]); };
		}
		else
		{
			alertLink.onclick = function() { alert('ODD: ' + nums[i]); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

Can you guess what it would generate? Quite a few undefines alerts, as a matter of fact. Why is that? Because the anonymous function is a closure, which capture not the value of i, but the i variable itself.

This means when we click on a link that this method has generated, we use the last known value of i. Since we have exited the loop, i is actually 8.

Now, in C# we have the same problem, and we can solve it by introducing a temporary variable in the loop, so we change the code to look like this:

	
function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
		var tmpNum = nums[i];
		
		if( nums[i] % 2 == 0)
		{
			alertLink.onclick = function() { alert('EVEN: '+ tmpNum ); };
		}
		else
		{
			alertLink.onclick = function() { alert('ODD: ' + tmpNum ); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

Try to run it, and you'll get an.. interesting phenomenon. All the links will show tmpNum as 7. Again, we captured the variable itself, not its value. And in JS, it looks like you are getting the same variable in the loop, not a new one (this is absolutely the wrong way to describe it, but it is a good lie), like you would in C#.

What is even more interesting is that you would get the exact same result here:

	
function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
		
		if( nums[i] % 2 == 0)
		{
			var tmpNum = nums[i];
			alertLink.onclick = function() { alert('EVEN: '+ tmpNum ); };
		}
		else
		{
			var tmpNum = nums[i];
			alertLink.onclick = function() { alert('ODD: ' + tmpNum ); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

Here we have two different lexical scopes, with respectively different variables. Looks like it should work. But the lexical scope of JS is the function, not the nearest set of curly. Both tmpNum refer to the same variable, and as such, are keeping the last value in it.

If the lexical scope is a function, we need to use a function then. Here is a version that works:

	
function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
	
		if( nums[i] % 2 == 0)
		{
			var act = function(tmpEVEN)
			{
				alertLink.onclick = function() { alert('EVEN: '+tmpEVEN); };
			};
			act(nums[i]);
		}
		else
		{
			var tmpODD = nums[i];
			alertLink.onclick = function() { alert('ODD: ' + tmpODD); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

And that is it for today's JS lesson.

Cross Site Scripting

So I had to do it today, I had two pages, in two unrelated domains (foo.com and bar.com) and I had to open a page from one and interact with it. Security constraints disallow this, unfortantely. There are all sorts of ways around it, mostly focusing on proxies, but I didn't want to get into that for a simple page, so I decided to write my own stupid method to do it.

From foo.com, the calling page:

var url = "http://www.bar.com/someImportantPage.castle?id=15"&onCloseRedirectTo=" + 
		encodeURIComponent(window.location.href + 
"&returnUrl="+ encodeURIComponent(window.location.href) ); window.open(url);

And using JS injection for the called page (I have some limited control there), I put:

if(window.opener)
{
	var oldClose = window.close;
	window.close = function()
	{
		if(window.opener && window.returnValue )
		{
			var url = decodeURIComponent($.getURLParam('onCloseRedirectTo')) + 
							"&idToAdd=" + window.returnValue;
			window.opener.location.href = url;
		}
		oldClose();
	};
}

And voila, it works. I'll leave the how as an excersize for the reader. Suffice to say that if you want to add a local iframe to the mix you can even get it to work in an "ajaxian" fashion.

Handling javascript localization in Mono Rail

I run into the issue of having to alert the user of some error, and it brought home the fact that English & Hebrew are not easy to mix. In fact, code such as this is consider a very bad sign, usually code that you don't really want to touch, see or believe in:

image

So, we need some sort of a way to externalize those strings, even if the only language that this application is going to use is Hebrew, simply because of the pain of mixing the two together.

After thinking about it for a while, I decided to create this view (JavascriptResources/index.brail):

<%
for de in controller.Resources:
	resourceName = de.Key
%>
	var ${resourceName} = {
	<% for item in GetParameter(resourceName): %>
		${item.Key}: '${item.Value.Replace("'","\\'") }',
	<% end %>
		Empty: ''
	};
<%
end
%>

And here is the definition of the controller:

[Resource("Processes", "MyApp.Resources.Javascript.Processes")]
[Resource("Errors", "MyApp.Resources.Javascript.Errors")]
[Resource("Messages", "MyApp.Resources.Javascript.Messages")]
public class JavascriptResourcesController : Controller
{
	public void Index()
	{
		
	}
}

But what the hell does this do, anyway? Not much, actually, but it will iterate over all the registered resources for the controller, and output something that looks like this for each of those resources:

var Processes = {
 	EmploymentType: 'Employment Type',
	Details: 'Details',
	Dates: 'The dates',
	Empty: ''
};

The nice thing is that I can now write: alert(Errors.RecursiveError);*. As a side affect (as I said, the application is just in Hebrew so I don't care about that much), I also get the usual benefits of localization.

* You have to read Hebrew to get the joke, I am afraid.

Using partials in Web Forms

Partial is a MonoRail term to a piece of UI that you extract outside, so you can call it again, often with Ajax. This is something that is harder to do in WebForms. Yesterday I found an elegant solution to the problem.

ASPX code:

<div id="UserDetailsDiv">
   <ayende:UserDetails runt="server" ID="TheUserDetails"/>
</div>

User Control:

<div>
Name: <asp:label ID="Name" runat="server"/> <br/>
Email: <asp:label ID="Email" runat="server"/> 
</div>

Client Side Code:

function changeUser(newUserId, div)
{
	var srv = new MyApp.Services.UserDetails();
	srv.GetUserDetailsView(newUserId, onSucccessGetUserDetailsView, null, div);
}

function onSucccessGetUserDetailsView(response, userContext)
{
	var div = $(userContext);
	div.innerHTML = response;
	new Effect.Highlight(div);
}

Web Service Code:

[WebMethod(EnableSession = true)]
public string GetUserDetailsView(int userId)
{
	User user = Controller.GetUser(userId);
	//there may be a better way to do this, I haven't bothered looking
	UserDetails userDetails = (UserDetails)new Page().LoadControl("~/Users/UserControls/UserDetails.ascx");
	userDetails.User = user;
	userDetails.DataBind();
	using(StringWriter sw = new StringWriter())
	using(HtmlTextWriter ht = new HtmlTextWriter(sw))
	{
		userDetails.RenderControl(ht);
		return sw.GetStringBuilder().ToSTring();
	}
}

Bug hunting, looking around the box

I just spent way too long trying to figure out what is wrong with this code:

<script type="text4/javascript">

        function customerUpdated()

        {

            //dostuff

        }
</script>
<
asp:DropDown ID="Customers" runat="server" DataTextField="Name"   
       OnClientChanged="customerUpdated();" DataValueField="Id"/>

The answer is below... but do try to figure it out

.

.

.

.

.

.

.

.

Can you see the problem? Perhaps this will help:

<script type="text4/javascript">

Argh!!! I must have gone over it a hundred times and didn't see it.

ASP.Net Ajax vs. Unit Tests

By definition, unit tests should be fast. Integration tests should be as fast as possible as well. Almost by definition, Javascript is not fast. In the battle between ASP.Net Ajax and my Unit/Integration tests, so far the only loser it me. This is the point when I am going to stop dedicating any more time to making this work.

ASP.Net Ajax, Performance and Race Conditions, Oh MY!

I have just tracked down the Nthbug in my application that I can blame ASP.Net Ajax for. To be rather exact, I can blame ASP.Net Ajax performance, and to be absolutely clear, I am talking about initialization performance.

The main issue is that I keep running into is that it takes human noticable time for the extended behaviors to be set on the page. By human noticable time I means over a second. The immediate result is that users are able to interact with the page in unexpected ways. Leading to all sorts of reported bugs that are plain impossible to reproduce unless you are aware of it.

Case in point, I have a link that point to a page, and on that link, I have defined the ModalPopupExtender. In the popup, the user is expected to fill some values, after which it will post to the next page. Imagine my surprise when I get a bug report saying that the next page showing an error about missing details. I tested it, the QA tested it, everything seemed to work, but the bug kept popping up every now and then.

It took a while to understand that the key difference was how soon the link was pressed after the page was loaded. The QA saying "and now I am going to press this link" was usually just enough for ASP.Net Ajax to initialize itself correctly.

The reasoning for this is that JavaScript is inheritly slow, and ASP.Net Ajax groups all the initialization code at the bottom of the page, so it is the last thing that is being run.  This is on a dual core, 3.4Ghz and 4Gb development machine! I am sick and tired of handling race conditions as it is, the last thing that I need is trying to handle them in javascript!

JavaScript Race Conditions

About a week ago I posted how to handle Multiple Cascadiong Drop Downs using MS Ajax. That worked, except sometimes it didn't work.

Took me a while to figure out that there was a race  condition there. Let me tell you, Javascript in the browser is not the language for tracking down threading issues. At any rate, here is the fix.

function changeOnParentChangeOnProductsToAvoidRaceConditions()

{

      var behaviors =  Sys.UI.Behavior.getBehaviors($('<%=Benefits.ClientID%>'));

      if(behaviors == null || behaviors.legth==0)

      {

            //wait for the behavior to be defined.

            setTimeout("changeOnParentChangeOnPoliciesToAvoidRaceConditions()", 10);

            return;

      }

      for( var i=0; i<behaviors.length;i++)

      {

            var behavior = behaviors[i];

            //required to get around race condition

            behavior._oldBenefitsOnParentChange = behavior._onParentChange;

            behavior._onParentChange = function()

            {

                  if( $('<%= Insurances.ClientID %>').selectedIndex == 0)

                  {

                        return;

                  }

                  behavior._oldBenefitsOnParentChange ();

            }

      }

}

Event.observe(window, 'load', function() {

      changeOnParentChangeOnProductsToAvoidRaceConditions();

}); 

Multiple Cascading Drop Down Lists

I recently found myself needing to handle a case where I had three drop down lists, and I needed to setup cascading relationship between those:

 ThreeDropDowns.png

Using the CascadingDropDown extender really make this a breeze, except that I had additional requirement, the benefits drop down should be filtered by both policy and insurance. So, when a policy is selected, all the matching insurance types are filled as well as all the matching benefits for the policy. When an insurance is selected, the benefits list should contains only the benefits for this insurance type.

At first I tried to simply setup a new extender from benefits to policies, that worked, until I selected a value from the insurance list, after which trying to unselect a value would leave me with a disabled drop down. After a bit of fumbling, I came up with this code, that runs on the onChanged client-side event on the Insurances drop down list:

function updateBenefitsDropDownIfDisabled()

{

    if($('<%=Insurances.ClientID %>').selectedIndex == 0)

    {

       setTimeout("updateBenefitsByPolicy();",1);

      }

}

The reason that I am using setTimeout here is that I want the updateBenefitsByPolicy to run after all the associated event hanlders for the event has run ( including the one that disabled it ). Here is how updateBenefitsByPolicy works:

function updateBenefitsByPolicy()

{

      var behaviors =  Sys.UI.Behavior.getBehaviors($('<%=Benefits.ClientID%>'));

      for(var i=0;i<behaviors.length;i++)

      {

            var behave = behaviors[i];

            if(behave._name == "CascadingDropDownBehavior" &&

                  behave._parentControlID == '<%=Policies.ClientID %>')

            {

                  behave._lastParentValues = null;

                  behave._clearItems();

                  var updating = new Option();

                  updating.text = '<asp:Literal runat="server" Text="<%$ Resources:App, Updating %>"/>';

                  addToDropDown(dropDown,updating);

                  behave._onParentChange(null,null);

            }

      }

}

It uses internal variabled from the CascadingDropDown behavior, so it is a hack, but it works. It cycles through the list of behaviors for this element, searching for a CascadingDropDown whose parent is the policies drop down, then it clear the cache (forces it to go back to the server) and manually force an update on the cascade behavior.

Not the most elegant code, but it works...

Development stats

A single page.

  • 34 integration tests.
  • 432 lines of test code (excluding data setup and stuff move to test infrastructure).
  • 66 lines of code behind
  • 210 lines of Javascript
  • 641 lines of in the ASPX file (include the javascript above)
  • 221 lines of code in the the controller

A lot of time.

Excluding the ASPX stuff (which include extenders and other things I probably shouldn't exclude), this puts me at 497 (210 of which are javascript) lines of code vs. 432 lines of test. It is simply amazing how much time the UI can take. And how its demands can drive the rest of the system.

Code smell

Why does it have to be like this?

if($F('<%= PolicyDescription.ClientID %>')==

    '<asp:Literal runat="server" Text="<%$ Resources:PolicyResources, DescriptionNotFound %>"/>')

{

    alert('<asp:Literal runat="server" Text="<%$ Resources:PolicyResources, EnterValidPolicyNumberOrDescription %>"/>');

    return;

}

 

How to add Atlas ListSearch to a drop down via code


I wanted to add this functionality to all my drop down lists, but it turned out that this is surprisingly difficult to do.
I am using the normal "inherit from all the controls that you are using" model, and I put the following inside my Drop Down class:

protected override void OnInit(EventArgs ignored)
{
    Page.RegisterRequiresControlState(this);
    Page.Load += delegate
    {
        extender.ID = "ListSearch_" + ID;
        extender.PromptCssClass = "invisible";
        extender.TargetControlID = "dummy";
        extender.ResolveControlID += delegate(object sender, ResolveControlEventArgs e) { e.Control = this; };
        AddExtenderToParentFormOrUpdatePanel();
    };
} 

private void AddExtenderToParentFormOrUpdatePanel()
{
    Control ctrl = Parent;
    while( !(ctrl is HtmlForm) && !(ctrl is UpdatePanel))
        ctrl = ctrl.Parent;
    UpdatePanel panel = ctrl as UpdatePanel;
    if(panel!=null)
    {
        panel.ContentTemplateContainer.Controls.Add(extender);
    }
    else
    {
        ctrl.Controls.Add(extender);               
    }

}

Tags:

Published at

Removing the leaky abstractions from WebForms

My preferred approach to develop on the web is using MonoRail, but since I am doing a lot of work for clients, they sometimes get to choose the technology. My current project is using Web Forms and Atlas.  I already mentioned that I am using Igloo to provide a truer MVC support for WebForms, it is not as good as what MonoRail can provide, but it is nice. (The only problem is that I keep feeling that my controllers are getting too big and complex, I have finally finished doing a stint on the UI that left me little time for refactoring, but it is getting past time to do it).

I believe that I have already mentioned in the past that I am not particulary fond of Web Forms, it is a leaky abstraction, trying to bring a semblence of statefulness to something that is stateless by nature. I did some big projects with WebForms, and I was bitten really bad by some of the stuff that went under the hood.

At any rate, here is the stuff that we came up with for the project:

  • Disable viewstate. That means that I disable it at the web.config level, and never enable it. Like most ASP.Net developers, I have been fighting with ViewState for a long time. It means that controls "forget" their state, and that sometimes I need to maintain state on my own, but it means that I get a lot more control on what is going on in the application. For some annoying reason, I still get a _VIEWSTATE variable in the pages, I assume that it is the ControlState, but so far I haven't dug far enough to find yet. I got some worrying responses from team mates, about what the implications of this can be. I had to sit and show them with Fiddler what is going on the wire to make sure that they understand what is happening.
  • Understand how Update Panels work. They are really cool technology. I am usually critical of Microsoft, but I have to say that I like UpdatePanels, they are a good solution for a problem that most WebForms developers need to deal with.
  • Do not get the data from the controls, get it from the request. There are two reasons for this, the first reason for it is simple, it is possible to access the request object from everywhere, it is much harder (and not wise) to pass the controls references. The second reason is that the controls will lie to you about their state. I just run into a situation where a DropDown reported SelectedValue = "" when the selected item was the one defined on the aspx (with append data bound items set to true), while on the request it was "0", the value given in the aspx.
  • Do no processing the the code behind files. When my controllers do not get the data directly from the request object, they expose methods such as:
    public ICollection<Policy> GetPoliciesForCustomer(string maybeId);
  • Don't bother with the designer.
  • Consider using the AjaxToolkit where appropriate, but be aware that it is not mature yet. If it gives you the least bit of trouble, drop it and go to Google.
  • Watin is really cool.
  • Would you write the tests already?

It is not comprehensive list by any mean, but this is just a few things that were brought up.

Atlas Weight Issues: Take 2

I got some really great comments on my last post. Apperantly FireFox doesn't report on the wire numbers, so here are the numbers from Fiddler.

After clearing the cache, a page with a text box, ScriptManager and CalendarExtender:

Request Count:  17
Bytes Sent:  10,492
Bytes Received: 149,145

Refreshing the page (meaning we get stuff from the cache):

Request Count:  14
Bytes Sent:  9,382
Bytes Received: 31,932

That is still 31Kb that I am not sure where they are coming from. Based on the feedback from the last post, it looks like there are a few other things that need to be done.

  • Get AjaxToolKit that is compiled in release mode.
  • <compilation debug="false">
  • <

    scriptResourceHandler enableCompression="true" enableCaching="true" />

After clearing the cache, same page:

Request Count:  14
Bytes Sent:  9,282
Bytes Received: 122,254

Refresh the page (get stuff from cache):

Request Count:  14
Bytes Sent:  9,382
Bytes Received: 32,010

So, we still get 31Kb, but worse than that, we get 14 requests(!).

Same page, without CalendarExtender (no cache):

Request Count:  5
Bytes Sent:  2,806
Bytes Received: 56,702

With cache:

Request Count:  4
Bytes Sent:  2,482
Bytes Received: 23,873

The request that seems to be biggest (and apperntly uncached) is: /WebResource.axd?d=vreWgJm8KHsCnZnd2dT3Zg2&t=633067941986250000 and it contains javascript functions such as WebForm_PostBackOptions and friends.

For comparision purposes, I have removed the ScriptManager as well:

Request Count:  2
Bytes Sent:  978
Bytes Received: 2,540

And when it is cached:

Request Count:  1
Bytes Sent:  554
Bytes Received: 747

As far as I can tell, I am doing everything the way I should do it. But this also means that the issue is with the weight, but the request count. HTTP allows only 2 requests per host, and that means that in cases where just the scripts takes 14 requests, everything else that is Ajaxified will need to wait for them to complete.

The solution for me was to drop the CalendarExtender completely, I went with a simple javascript calendar, it doesn't do sliding animations and other cool stuff that the CalendarExtender does, but it doesn't kill my site when it is working, either.

Atlas weight issues: what am I missing?

I have the strong feeling that I am missing something. We have just run into a major slow down in one of our pages, as a result of adding the CalendarExtender. This led me to do some searching, and I am not sure that I can believe the results.

Let us take this simple page:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="test.aspx.cs" Inherits="ExplodingMonkeys.Web.test" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

    <title>Untitled Page</title>

</head>

<body>

    <form id="form1" runat="server">

        <asp:TextBox ID="Date" runat="server" />

    </form>

</body>

</html>

When rendered to the browser, it weight 528 bytes. Now, let us add a <asp:ScriptManager runat="server"/> to the page. The size of the page exploded to 79 kilobytes, from this change alone.

I then added the following to the page:

<ajax:CalendarExtender runat="server" TargetControlID="Date" CssClass="ClassName" Format="MMMM d, yyyy"
PopupButtonID="http://ajax.asp.net/ajaxtoolkit/Calendar/Calendar_scheduleHS.png" />

The page size reached 143 kilobytes. I am pretty sure that I am missing something here. I enabled both caching and compression in the web config:

<

scriptResourceHandler enableCompression="true" enableCaching="true" />

And then tried again. The results were better, but not really encouraging.

With just script manager: 79 KB (56 KB from cache)
With calendar extender: 143 KB (115 KB from cache)

Just to make it clear, there is a noticable delay on our pages, it went from being instantenous to taking four or five seconds. And I am talking about localhost testing, on powerful machines. (Again that HTTP 2 connections per server issue, I think).

Am I missing something, or are those the real numbers?

Evaluating Atlas

Sorry, I meant that I am evaluating Microsoft(R) ASP.Net(TM) Ajax(Patent Pending) Extentions :-).

I mentioned before that I am not really feeling overly excited about that whole Ajax thing. Because of a client constraints, I am unable to use MonoRail (but I use just about everything else from the Castle stack :-) ), so I thought that I would give Atlas a try. This is still very early in the project, but I like to understand how things works so I know why they broke. And for some reason, just about anything breaks when I start playing with it.

At any rate, I took the cascading style list demo for a spin, and managed to hit issues with its error reporting almost immediately. I didn't specify the full path to the service, which meant that it had very helpfully showed me a [method error 500], which I was not pleased to see. Maybe there is a way to get more infomration from it, but I couldn't find it.

Once I got over this hurdle, it was much easier. This is a fairly core scenario, so I wasn't surprised that it was easy, just wire the extenders to the correct controls and write the service methods. Here is my code:

public class NewPolicyRequestService : BaseScriptService

{

    private NewPolicyRequestController controller;

 

    public NewPolicyRequestController Controller

    {

        get { return controller; }

        set { controller = value; }

    }

 

    [WebMethod]

    public CascadingDropDownNameValue[] GetOptionsForPolicy(

        string knownCategoryValues,

        string category)

    {

        StringDictionary kv = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);

        string maybePolicyId = kv["Policies"];

        ICollection<PolicyOptions> options = Controller.GetOptionsForPolicy(maybePolicyId);

        List<CascadingDropDownNameValue> values = new List<CascadingDropDownNameValue>(options.Count);

        foreach (PolicyOptions option in options)

        {

            values.Add(new CascadingDropDownNameValue(option.Name, option.Id.ToString()));

        }

        return values.ToArray();

    }

 

    [WebMethod]

    public CascadingDropDownNameValue[] GetPolicies(

        string knownCategoryValues,

        string category)

    {

        ICollection<Policy> policies = Controller.GetPolicies();

        List<CascadingDropDownNameValue> values = new List<CascadingDropDownNameValue>(policies.Count);

        foreach (Policy policy in policies)

        {

            values.Add(new CascadingDropDownNameValue(policy.Name, policy.Id.ToString()));

        }

        return values.ToArray();

    }

}

Couple of things to note here, BaseScriptService has all the appropriate attributes [ScriptService], [WebService], [Etc] :-). It is also responsible for injecting the correct controller, which is the one who is responsbile for the actual work. The service is here merely to unpack the data from the request and then translate the returned results to something that Atlas can understand.

So far, very easy. I don't like the fact that I have to duplicate code here, though. There may be a better way, but the only thing that I can think about is to add a template method that accept a couple of delegates, and I don't see that as adding to the maintainability of the system.

The more important concern that I have, though, is what happen when I need to extend this example. Imagine a scenario where I have this:

(Image from clipboard).png

When a policy is selected, I need to update the child drop down, but I also need to update the policy description. I know how I can do it in MonoRail (easy), and I know how I can do it in using HTML/Javascript (not tough). I have no idea how hard it would be to make it work with ASP.Net/Atlas.*

I figure that I'll probably need to drop the drop down extenders completely (and handle all the updates to the drop downs myself {I have way more than 2, by the way} ), in order to be able to recieve more information than what the extender gives me. More annoying then that, I would have to use the horrible <%= Policies.ClientID %> all over the place, and get the lovely "Control Collection Could Not Be Modified" exceptions when I try to do more advance stuff.**

* I find it sad that I need to seperate ASP.Net from HTML

** Experiance hurts, I suggest avoiding it.

With or without you: Ajax

David Hayden has a post (ASP.NET AJAX Web Controls - AJAX Experience Without Learning AJAX) that really bothers me. He talks about the different components that he tested for Ajax support, and conclude with:

I was thinking that the AJAX experience was going to be a lot of work, but my guess is that the control vendors are taking care of this for me. And, quite frankly, I didn't want to have to learn this technology in depth if I could get away with it.

Frankly, I would like to know as much about Ajax as I could get away with. The problem is that you can't escape from the complexity. You can wrap it, hide it, stash it in the attic and throw away the key, but it will come back and hunt you.

(Image from clipboard).png Complexity, meet Ajax, you are going to be best friends.

The law of leaky abstractions holds quite well for Ajax. Eventually, you will need to do something slightly beyond what is supported by the controls, or you will get an error that can't be fixed by setting a property somewhere, or you'll need to debug an issue. Those are the cases where the willful ignorance will come to haunt you.

Tags:

Published at