Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

December 2005 - Posts

Saving partial state of an ASP.NET control

Rick Strahl has a created a great control that's specialized in saving other control's state. This enables you to declare what properties you really care about, which is a great improvement over ViewState and even ControlState.
There could certainly be a few improvements, but check it out:
http://west-wind.com/weblog/posts/3988.aspx
UPDATE: Rick did actually improve his control based on the feedback he got and now it looks perfect:
http://west-wind.com/weblog/posts/4094.aspx

Atlas December available; Event bubbling in Atlas

You can download the December release of Atlas from this URL:

http://msdn.microsoft.com/asp.net/info/future/atlastemplate/

Nikhil has already published a detailed post about the main new feature of this release, namely the new server control model:

http://www.nikhilk.net/AtlasM1.aspx

There are a few other changes besides bug fixes in this release. Here's one: event bubbling.

In previous releases, if you wanted a button in a master list to change the currently selected item in a details view, you had to do this:

<button id="myButton">
  <click>
    <setPropertyAction target="details" property="dataIndex">
      <bindings>
        <binding dataPath="sender.dataContext._index" property="value"/>
      </bindings>
    </setPropertyAction>
  </click>
</button>

That was convoluted, required intimate knowledge of the inner workings of data binding, events and DataRow. It was overly complex for such a small task. Now, you just need to know that both ListView and ItemView have a dataIndex property (this may change to selectedIndex in a future release) and that ListView has a "select" command.

So now all you need to do is to bind the details and master views together using a simple binding, for example in your master view, so that their selected items are synchronized:

<binding property="dataIndex" dataContext="details" dataPath="dataIndex" direction="InOut"/>

And then you can just add a simple button to your ListView template:

<button command="select" id="selectButton"/>

Event bubbling is of course not limited to this simple application. You can leverage it in your own components. This process is twofold.

First, the component that wants to raise a bubbled event (like button is doing in our example) needs to call raiseBubbleEvent, a new method on Control. The second argument of this call must derive from Web.UI.CommandEventArgs, which enables you to specify a command name and an optional command argument.

Second, the control that wants to handle bubbled events (like ListView is doing for the "select" command) needs to override onBubbleEvent (also a new method on Control). This method should look at the command name from the event argument and handle the commands it cares about. If it did successfully handle a command, it should return true so that the event stops bubbling up the control hierarchy. Otherwise, it should return false. In ListView, the event handler looks at the source or sender of the event (the button in our example) and gets the index from its data context (the current row in our example).

Those of you who are familiar with server-side event bubbling in ASP.NET will recognize that we used the exact same pattern here and that the methods and parameters share the same object model as server-side.

Man literally dives from sky
I have to admit I didn't know that.
In 1960, before Gagarin, Joe Kittinger reached the boundary between Earth and space using a balloon. He reached the incredible altitude of 31300 meters, 3.5 times the height of Mount Everest. It's not technically space (if a ballon can sustain itself, it means that there still is enough atmosphere for it to float) but it's admittedly a fuzzy limit.
That's fascinating in itself, but wait... Once he got there, he did the most amazing thing: he jumped. He... jumped... With a movie camera. Having jumped from a much more modest altitude, I can only begin to imagine the life-altering experience it must have been for him. Amazingly, we have images of his incredible dive.
That day, Joe Kittinger, at the peril of his life, advanced the whole of the human kind on its way into space, and broke four records: highest balloon ascent, highest parachute jump, longest freefall and fastest speed by a man through the atmosphere at a whopping 982 km/h.
Watch the video here: Skydiving from the edge of the world
Seen on Nobel Intent.
Making callbacks (and Atlas) synchronous, or how to shoot yourself in the foot

I've explained before why XmlHttpRequest should always be used asynchronously. In a nutshell, JavaScript is not multi-threaded, so the only way to keep your application and browser reasonably responsive is to use some kind of asynchronous pattern. This way, the multitasking is left to the hosting browser and the JavaScript developer can enjoy a relatively easier programming environment where he only needs to care about events and not about summoning threads and managing locks.

It's important to note that if you click on a link in a browser, it usually doesn't freeze: the UI is still fully usable even while the request is being completed. You can still cancel it by pressing the stop button, you can access all the menus, etc.

While there is a synchronous XmlHttp request going on, it's a different matter: the browser is completely frozen and none of the UI works. This is utterly wrong on several accounts.

First, if the server never answers, your users will need to kill the browser (assuming they know how to do that, which they usually don't).

Second, any UI that freezes for more than half a second without giving the user a clue about what's going on (remember the little animation that usually indicates navigation or posting back does not move during an XmlHttp request), as far as the user is concerned, just looks as if it had crashed.

Finally, the web application should not have side effects on its container (the browser). In particular, it should not put it into an unresponsive state. I agree that the browser should not let itself be frozen by its contents, but that's unfortunately the way it is and we just have to deal with it (by the way, Firefox reacts exactly the same way as IE in this department).

That's why callbacks in both ASP.NET 2.0 and Atlas are always asynchronous. The async parameter in the case of ASP.NET callbacks is mileading. It should really be named "parallel": when set to true, any number of callbacks may be initiated simultaneously and if false, only the last initiated will actually call back.

That being said, I've been getting a lot of feedback lately from people who just dislike so much asynchronous programming that they want nothing to do with it (even if it's conveniently hidden from them like it is in Atlas). Well, if what you really want is to shoot yourself in the foot, who am I to argue with that? You're the customer, and I'm here to answer your demands. So here's the gun... (of course I'm kidding here. I understand why people want to use synchronous callbacks even if I personally disagree).

Add this small script to your page (preferably in the <head> section) and all your XmlHttp requests will be done synchronously no matter what the framework you're using is doing. This works with ASP.NET 2.0 callbacks in both IE and Firefox but breaks callbacks for Opera. I suspect that it would also work with other Ajax frameworks such as Atlas.

<script type="text/javascript">
    var __xmlHttpRequest = window.XMLHttpRequest;
    window.XMLHttpRequest = XMLHttpRequest = function() {
        var _xmlHttp = null;
        if (!__xmlHttpRequest) {
            try {
                _xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch(ex) {}
        }
        else {
            _xmlHttp = new __xmlHttpRequest();
        }
       
        if (!_xmlHttp) return null;
        
        this.abort = function() {return _xmlHttp.abort();}
        this.getAllResponseHeaders = function() {return _xmlHttp.getAllResponseHeaders();}
        this.getResponseHeader = function(header) {return _xmlHttp.getResponseHeader(header);}
        this.open = function(method, url, async, user, password) {
            return _xmlHttp.open(method, url, false, user, password);
        }
        this.send = function(body) {
            _xmlHttp.send(body);
            this.readyState = _xmlHttp.readyState;
            this.responseBody = _xmlHttp.responseBody;
            this.responseStream = _xmlHttp.responseStream;
            this.responseText = _xmlHttp.responseText;
            this.responseXML = _xmlHttp.responseXML;
            this.status = _xmlHttp.status;
            this.statusText = _xmlHttp.statusText;
            this.onreadystatechange();
        }
        this.setRequestHeader = function(name, value) {return _xmlHttp.setRequestHeader(name, value);}
    }
</script>

Update: modified the script to include more properties of the XHR object, which makes the script compatible with ASP.NET 3.5.

Another Atlas keyboard behavior?

I wrote this keyboard behavior a while ago and I just realized that Wilco published something very similar long before me. Oh well, just use the one you like best...

Here's how you use the behavior:

<textBox id="tb">
  <behaviors>
    <keyPressBehavior key="Esc" keyPress="onEscKeyPress"/>
    <keyPressBehavior key="b" shiftKey="true" keyPress="onShiftBKeyPress"/>
  </behaviors>
</textBox>

You can also get all keypresses if you don't specify the key.

Here's the source code for it:

Web.UI.Key = {
    Backspace: 8,
    Tab: 9,
    Return: 13,
    Esc: 27,
    PageUp: 33,
    PageDown: 34,
    End: 35,
    Home: 36,
    Left: 37,
    Up: 38,
    Right: 39,
    Down: 40,
    F1: 112,
    F2: 113,
    F3: 114,
    F4: 115,
    F5: 116,
    F6: 117,
    F7: 118,
    F8: 119,
    F9: 120,
    F10: 121,
    F11: 122,
    F12: 123,
    Delete: 127
}; // del is 46 on Windows

Web.UI.Key.parse = function(s) {
    if (s.length == 1) {
        return s;
    }
    var result = parseInt(s);
    if (!isNaN(result)) {
        return result;
    }
    for (var f in this) {
        if ((f == s) && (typeof(this[f]) == 'number')) {
            return this[f];
        }
    }
    throw 'Invalid Key Value';
}

Web.UI.Key.toString = function(value) {
    if (typeof(value) == 'string') {
        return value;
    }
    for (var v in this) {
        if (this[v] == value) {
            return v;
        }
    }
    return value.toString();
}

Web.UI.KeyPressEventArgs = function(key, shiftKey, controlKey, altKey) {
    Web.UI.KeyPressEventArgs.initializeBase(this, []);
   
    var _key = key;
    var _shiftKey = shiftKey;
    var _controlKey = controlKey;
    var _altKey = altKey;
   
    this.get_altKey = function() {
        return _altKey;
    }
   
    this.get_controlKey = function() {
        return _controlKey;
    }
   
    this.get_key = function() {
        return _key;
    }
   
    this.get_shiftKey = function() {
        return _shiftKey;
    }
   
    this.getDescriptor = function() {
        var td = Web.UI.KeyPressEventArgs.callBaseMethod(this, 'getDescriptor');
       
        td.addProperty('altKey', Boolean, true);
        td.addProperty('controlKey', Boolean, true);
        td.addProperty('key', Web.UI.Key, true);
        td.addProperty('shiftKey', Boolean, true);
       
        return td;
    }
    Web.UI.KeyPressEventArgs.registerBaseMethod(this, 'getDescriptor');
}
Type.registerClass('Web.UI.KeyPressEventArgs', Web.CancelEventArgs);

Web.UI.KeyPressBehavior = function() {
    Web.UI.KeyPressBehavior.initializeBase(this);
   
    var _keyHandler;
    var _key;
    var _shiftKey;
    var _controlKey = false;
    var _altKey = false;

    this.get_altKey = function() {
        return _altKey;
    }
    this.set_altKey = function(value) {
        if (_altKey != value) {
            _altKey = value;
            this.raisePropertyChanged('altKey');
        }
    }

    this.get_controlKey = function() {
        return _controlKey;
    }
    this.set_controlKey = function(value) {
        if (_controlKey != value) {
            _controlKey = value;
            this.raisePropertyChanged('controlKey');
        }
    }

    this.get_key = function() {
        return _key;
    }
    this.set_key = function(value) {
        if (_key != value) {
            if (this.get_isInitialized()) {
                disposeKeyHandler.call(this)();
                _keyHandler = Function.createDelegate(this, keyHandler);
                this.control.element.attachEvent((typeof(value) == 'number') ? 'onkeyup' : 'onkeypress', _keyHandler);
            }
            _key = value;
            this.raisePropertyChanged('key');
        }
    }

    this.get_shiftKey = function() {
        return _shiftKey;
    }
    this.set_shiftKey = function(value) {
        if (_shiftKey != value) {
            _shiftKey = value;
            this.raisePropertyChanged('shiftKey');
        }
    }
   
    this.getDescriptor = function() {
        var td = new Web.TypeDescriptor();
       
        td.addProperty('key', Web.UI.Key);
        td.addProperty('shiftKey', Boolean);
        td.addProperty('controlKey', Boolean);
        td.addProperty('altKey', Boolean);
        td.addEvent('keyPress', true);
        return td;
    }
   
    this.keyPress = this.createEvent();
   
    this.dispose = function() {
        disposeKeyHandler.call(this);
        Web.UI.KeyPressBehavior.callBaseMethod(this, 'dispose');
    }
   
    function disposeKeyHandler() {
        if (_keyHandler) {
            this.control.element.detachEvent((typeof(_key) == 'number') ? 'onkeyup' : 'onkeypress', _keyHandler);
            _keyHandler = null;
        }
    }

    this.initialize = function() {
        Web.UI.KeyPressBehavior.callBaseMethod(this, 'initialize');

        _keyHandler = Function.createDelegate(this, keyHandler);
        this.control.element.attachEvent((typeof(_key) == 'number') ? 'onkeyup' : 'onkeypress', _keyHandler);
    }
   
    function keyHandler() {
        var evt = event;
        var keyCode = evt.charCode ? evt.charCode : evt.keyCode;
        // Special-case delete as the Windows delete key has a non-standard keyCode.
        // See http://www.w3.org/TR/1999/WD-DOM-Level-2-19990923/events.html#Events-KeyEvent for a list of standard key codes.
        // See http://www.w3.org/2002/09/tests/keys.html for a keyCode testing tool.
        // Cancelling for Delete doesn't work in IE.
        if ((keyCode == 46) && (_key == Web.UI.Key.Delete)) {
            keyCode = Web.UI.Key.Delete;
        }
        if ((evt.altKey == _altKey) &&
            (evt.ctrlKey == _controlKey) &&
            ((_shiftKey == null) || (typeof(_shiftKey) == 'undefined') || (evt.shiftKey == _shiftKey)) &&
            (((typeof(_key) == 'number') && (keyCode == _key)) ||
            ((typeof(_key) == 'string') && (String.fromCharCode(keyCode).toLowerCase() == _key.toLowerCase())) ||
            (_key == null) || (typeof(_key) == 'undefined'))) {
           
            var args = new Web.UI.KeyPressEventArgs(_key ? _key : String.fromCharCode(keyCode), evt.shiftKey, evt.ctrlKey, evt.altKey);
            this.keyPress.invoke(this, args);
           
            if (args.get_canceled()) {
                event.returnValue = false;
                event.cancelBubble = true;
            }
        }
    }
}
Type.registerSealedClass('Web.UI.KeyPressBehavior', Web.UI.Behavior);
Web.TypeDescriptor.addType('script', 'keyPressBehavior', Web.UI.KeyPressBehavior);

Got me a 360 this morning

Well, it seems like this second XBOX 360 shipment has finally arrived. Thanks to a tipster whose identity I shall not reveal (but absolutely no MS insider info involved), I was this morning a little before 9AM (one hour before the gates open) at the Costco in Bellingham. We were second in line with my friend David. The line was no more than 25 people when the gates opened, and they had dozens of 360s ready to grasp. Rumor has it they had 144 in Bellingham and about as many in Tumwater.

The bundle we got was the premium package plus a second wireless controller, the play & charge and PGR3 for $479 plus tax.

I got a 360! I got a 360! I got a 360! I got a 360! I got a 360! I got a 360! I got a 360!

Update: corrected the bundle price, which was obviously wrong.

Find me on the new Windows Live Local
I think this is pretty neat (click on the bird's eye icon, that's the really awesome one): This is where I work
Date is a reference type in JavaScript

If you try this:

(new Date(2000, 0, 1)) == (new Date(2000, 0, 1))

you may be surprised to learn that it is actually false, because they are different object references, even if the values are the same. If you want to check equality of two dates, you need to compare the values instead of the Date objects:

(new Date(2000, 0, 1)).valueOf() == (new Date(2000, 0, 1).valueOf())

Hope this helps.

How to use enumerations in Profile?

If you've tried to put an enum type into the ASP.NET Profile, maybe you've noticed that there's a small caveat with specifying its default value. To specify the default value, you need to use the serialized value, using the same serialization the profile is going to use for this property. For non-string types, the default serializer is XML. So if you add this to your profile section:

<add name="FavoriteDay" type="System.DayOfWeek" defaultValue="Saturday"/>


You'll get some weird XML exception when you try to use that profile property. You could specify the default value in XML serialized format, but there's not much point in doing that. It's much easier to change the serialization format for this property to string:

<add name="FavoriteDay" type="System.DayOfWeek" defaultValue="Saturday" serializeAs="String"/>


Thanks to Stefan Schackow for explaining this.

Google Sitemaps for ASP.NET 2.0

Google has a little-known feature that enables web site authors to tell the search engine about the structure of their site. ASP.NET also has a way of representing the structure of the site in a map. ASP.NET SiteMaps can be exposed on the site using a Menu, TreeView or SiteMapPath control, but there is no built-in feature that exposes this information in the format that Google understands.

That's why I've developed a small handler that scans the ASP.NET SiteMap and formats it into the Google XML format for site maps. Once you've copied this handler into your site, just register it as your site map with Google from this page:
https://www.google.com/webmasters/sitemaps

I've also added a very simple control that displays the ASP.NET SiteMap in a very plain HTML form (using h1...h6 tags) that is likely to be well indexed by most search engines. The control's look can be very much improved on and customized but I'll leave that as an exercise for the reader.

The sample also comes with a sample site to demonstrate the use of the control and handler.

Download the project from here:
http://www.gotdotnet.com/codegallery/codegallery.aspx?id=237330e0-65c5-4ebb-b62b-e486dd598604

UPDATE: I updated the handler so that it can now use Google-specific attributes. You can now specify additional information such as the update frequency of each page directly in the ASP.NET SiteMap (version 1.1 of the handler).

UPDATE 2: This implementation is now obsolete. This feature is now part of the ASP.NET Futures release with a much more complete implementation. Check it out!
http://www.asp.net/downloads/futures/default.aspx?tabid=62

UPDATE 3: I've added back the source code as an attachment for this post, for reference purposes (but I encourage everyone to use the Futures version).

More Posts Next page »