Tuesday, May 29, 2012

Pitfall in Responding to Different Views With Windows 8 WinJS Solution

It's recommended to use CSS to update the layout using CSS media selector, for instance, following css defines two sizes for the column class, one for landscape and the other for portrait:
@media screen and {-ms-view-state: fullscreen-portrait} {
    .column { width: 400px;}
} 
@media screen and {-ms-view-state: fullscreen-portrait} {
    .column { width: 250px; }
}
But in some situations the page layout as well as content vary based on the view-state (this view-state refers to the screen view state for a win8 device, totally different concept from the ASP.NET view state), and CSS is not capable of handling that. In such case, we usually use DOM window's resize event for notification. Traditional way of doing that is assign the handler as below:
window.onresize = function () { // Do stuff based on current view-state};
The handler still works in WinJS but it would override other handlers. A better way is to use addEventListener:
window.addEventListener("resize", pageLayoutChanged); 
function pageLayoutChanged() {
    // Do stuff based on current view-state
}
With addEventListener you can define multiple handler functions and they will all be invoked when event is triggered (sequence not determined). Be aware that this window object is global. So all registered event handlers will be invoked when you navigate to different pages. For example, you have page1LayoutChanged event in page1 and page2LayoutChanged event in page2. When the screen's view-state is changed, both page1LayoutChanged and page2LayoutChanged function will be triggered to run no matter when accessing page1 or page2.

This behavior is totally fine in Web application where new DOM objects and Javascript references are constructed when a page is loaded from server and rendered inside the browser. That's not the case for a WinJS app since all Javascript closures are still effective when running inside WinJS context. This may cause problem sometimes. Let's look at an example here. References to page1 local objects inside page1LayoutChanged function will be null when you are on page2. So we need to have some logic to check if it's current page inside the event handler function in this case. An easy way is check if a page DOM element exists or not:
// page1 view-state change event handler
function pageLayoutChanged() {
    var pageElement = document.getElementById("page1Header");
    if (pageElement) { 
        // Do stuff based on current view-state
    }
}
If you happen to use WinJS Navigation App template in which a pre-defined resize handler inside navigator.js will check and call a "updateLayout" page function, so you could just your add view-state change logic inside the updateLayout function without registering resize event handler:
updateLayout: function (element, viewState, lastViewState) {
    var pageElement = document.getElementById("page1Header");
    if (pageElement) { 
        // Do stuff based on current view-state
    }
}
Another note is that we use document.getElementById function to get a DOM element from the page. Actually there's a short-cut for element selector in WinJS app: simply reference any DOM element in Javascript by its unique ID. So following Javascript is equivalent to above version:
updateLayout: function (element, viewState, lastViewState) {
    if (page1Header) { 
        // Do stuff based on current view-state
    }
}