Oren Eini

CEO of RavenDB

a NoSQL Open Source Document Database

Get in touch with me:

oren@ravendb.net +972 52-548-6969

Posts: 7,598
|
Comments: 51,226
Privacy Policy · Terms
filter by tags archive
time to read 8 min | 1526 words

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();

}); 

time to read 9 min | 1653 words

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...

time to read 1 min | 182 words

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

time to read 2 min | 370 words

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;

}

 

time to read 2 min | 287 words


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);               
    }

}

time to read 4 min | 716 words

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.

time to read 4 min | 718 words

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.

time to read 7 min | 1362 words

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

time to read 19 min | 3681 words

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.

FUTURE POSTS

  1. Memory optimizations to reduce CPU costs - one day from now
  2. AI's hidden state in the execution stack - 4 days from now
  3. The role of junior developers in the world of LLMs - 6 days from now

There are posts all the way to Aug 20, 2025

RECENT SERIES

  1. RavenDB 7.1 (7):
    11 Jul 2025 - The Gen AI release
  2. Production postmorterm (2):
    11 Jun 2025 - The rookie server's untimely promotion
  3. Webinar (7):
    05 Jun 2025 - Think inside the database
  4. Recording (16):
    29 May 2025 - RavenDB's Upcoming Optimizations Deep Dive
  5. RavenDB News (2):
    02 May 2025 - May 2025
View all series

Syndication

Main feed ... ...
Comments feed   ... ...
}