Asked by:
ListView item template & WinJS.Binding.Initializer: Creating DOM objects within the item template only works when using Promise timeout

Question
-
Hi folks. I am editing the Controls ListView Basic example from MSDN. This creates a grid of items, each item contains an image and overlay text. I'm switching out the image in the template to a chart. This chart is just a div which I pass to the jqPlot javascript charting library's initialize function to generate a graph. To draw the graph, jqPlot creates DOM elements within that div.
The problem I am having is that jqPlot errors out when I attempt to call it from my WinJS.Binding.Initializer -- *unless* I set a timeout to delay creation of the chart. Basically in the Binding.Initializer, I attempt to create the plot by passing the DIV to jqPlot's initialize function. If this is done immediately, the call fails-- I've traced through the library and it is having difficulty finding the DIV dom element (it seems like perhaps the page isn't yet fully ready). If I wrap the call to initialize the chart in a Promise.timeout, it works fine. Waiting a fixed amount of time before drawing the charts seems like a really bad solution. There must be some event that has to occur before I can draw these graph-- what is it, and how do I have my code wait for that event before attempting to draw the graph?
Note that I'm using jQuery.
Here's my code:
This is my template. Note the data-win-bind on the div with class "item-image":
<div class="itemtemplate" data-win-control="WinJS.Binding.Template"> <div class="item"> <div class="item-image" symbol="#" data-win-bind="id: symbol; innerHtml: symbol InitChart.getChartBody"></div> <div class="item-overlay"> <h4 class="item-title" data-win-bind="textContent: title"></h4> <h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6> </div> </div> </div>
Ok, now let's take a look at the Binding Initializer, getChartBody:
WinJS.Namespace.define("InitChart", { getChartBody: WinJS.Binding.initializer(function (source, sourceProp, dest, destProp) { if (source.chartName) { $(dest).attr('chartName', source.chartName); dest.id = "chart" + source.chartName; if ($('#'+dest.id)) WinJS.Promise.timeout(2500).then(function(c) { console.log("Timeout expired..."); var plot = $.jqplot(dest.id, [[[1, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]]]); }); else console.log("Can't find DOM element"); } }) });
The source data looks very much like the original example...
var sampleItems = [ { group: sampleGroups[0], title: "Fun title", chartName: 'fun', subtitle: "Item Subtitle: 1", description: itemDescription, content: itemContent, backgroundImage: lightGray }, { group: sampleGroups[0], title: "Sad title", chartName: 'sadness', subtitle: "Item Subtitle: 2", description: itemDescription, content: itemContent, backgroundImage: darkGray }, ... so on ...
Thoughts? Ideas? Suggestions?
I really don't like the idea of using a 1 second timeout or something... it's a hack, and I just need to understand what's going on behind the scenes in terms of events...
- Edited by Peter Vieth Tuesday, April 9, 2013 4:36 PM typo in copied source
Tuesday, April 9, 2013 4:35 PM
All replies
-
How and when are you calling WinJS.UI.process[All] and WinJS.Binding.processAll? As both of these are async operations, my guess is that UI.processAll hasn't finished getting the controls in the DOM before Binding.processAll is attempting to resolve the data-win-bind attributes. This would explain why a timeout delay solves the problem.
In that case you'd need to kick off Binding.processAll within a completed handler on UI.processAll, e.g.
WinJS.UI.processAll().then(function () { WinJS.Binding.processAll(/* args */); });
Kraig
Author, Programming Windows 8 Apps with HTML, CSS, and JavaScript, a free ebook from Microsoft Press
Tuesday, April 9, 2013 5:58 PM -
The UI.processAll is in default.js's addEventListener "activated". I can't find Binding.process or processAll in the project. Yet the bindings do get processed.
Tuesday, April 9, 2013 6:36 PM -
Ok, more details. The affected page is groupedItems.html. In groupedItems.js in Pages.define ready function, the listview has some properties set (like the template) and then is initialized like so:
this._initializeLayout(listView, appView.value);
I assume this loads the template and does all the binding somewhere in its bowels.
Tuesday, April 9, 2013 6:41 PM -
Ok, good progress today. I appears I need to use an event other than ready, or figure out how to pass "element" from the ready function to my binding function. Or just use a really short timeout.
I changed the code to not load my sample data immediately. I bind to an empty list, then in the ready event of the page, I load the sample data. However, a timeout is still necessary.
Out of curiosity, I started ramping the timeout delay down... way down. Even setting a timeout of 1ms made the charts draw correctly.
So I also tried another charting library. Identical results. This library draws via SVG, which I can inspect in the DOM explorer, which is a nice bonus. Using the DOM explorer, I can see that without the timeout, all the charts are created, however, the path data is truncated and all zeroes. The first line of the chart creation routine checks the container's (a DIV) height and width and interpolates the source data appropriately. Ahh... Well, the DIV's width is null and height is 0. Hence the interpolation just wipes out all the data.
If timeout is even 1ms, the DIV's height and width match the stylesheet, so the interpolation succeeds.
After Googling around it seems that I'm not supposed to query the document in ready, but use the "element" argument passed in. I'm not really clear on how I should pass this to the binding function, so I may just live with a timeout of 1ms. Thoughts?
Thursday, April 11, 2013 7:05 AM -
Sorry to be slow in responding, but to answer your questions.
First, WinJS.UI.processAll is called automatically for page controls, and I believe the binding process is done in there as well, which is why you don't see it.
Second, the _initializeLayout method is in groupedItems.js directly--it's just part of the template code.
Next, responding to your last post, you could try calling WinJS.Binding.ProcessAll(element, <data>) in the ready method, where <data> is the object like sampleItems that you'd binding to. That's how you say "do the binding on this element" directly.
If you're finding that a really short timeout does the trick, then it seems to be an issue with yielding the UI thread to complete the layout. If you still need a timeout, the right call to use here is setImmedate, which yields without a specific time interval. Still, doing the binding directly within ready might solve it.
Otherwise, I'd love to see a small repro project if you have one.
Friday, April 12, 2013 8:17 PM -
I assume you're using this in the context of a ListView or FlipView?
The items within those controls are rendered when they aren't attached to the DOM so things like lookup by ID won't find them. What you're seeing is that if you wait long enough you will find that the items are now attached to the DOM.
Is there a way to pass the element to this charting function instead of the ID?
-josh
Sunday, April 14, 2013 4:42 AM