locked
Data Binding: Updating an Item in a ListView after Async Call fails

    Question

  • Hello everyone,

    after searching around for hours I have finally decided to ask my question here. I have a ListView and a simple food item class:

    // This class displays food items
    (function () {
        var Food = WinJS.Class.define(function Food(name, user, image) {
            // Constructor
            if (arguments.length < 3)
                return false;
            this._initObservable();
            this.name = name;
            this.user = user;
            this.image = image;
        }
        );
    
        WinJS.Class.mix(Food,
                WinJS.Binding.mixin,
                WinJS.Binding.expandProperties({ name: "", user: "", image: "" })
            );
    
        WinJS.Namespace.define("Model", {
            Food: Food
        });
    })();


    The images are dynamically loaded from Flickr. I add items to the list while the correlating image is still loaded from the API using XHR.

    So what happens is:

    // This is a async call, image is still undefined at this point
    var image = ViewModel.fetchImagePath(text);
    // Instantiate the object
    var food = new Model.Food(text, user, image);
    // Push it to the List
    this.ingredients.push(food);

    The object is perfectly added to the list and shows up in the UI's ListView. However when the async call finishes a second later the image property is not updated (frankly I am not sure if it's not updated in the UI or also in the model). I think I am missing something but I have no idea what as the whole thing is pretty new to me.

    I have read all the guides (like the one for complex object binding) but I am stuck here. I would appreciate every single helpful comment.

    Cheers,

    Fred


    • Edited by Freddixx Tuesday, January 15, 2013 3:22 AM
    Tuesday, January 15, 2013 3:19 AM

Answers

  • Maybe the solution can be achieved as a Promise:

    // Instantiate the object
    var food = new Model.Food(text, user, image);
    //assuming "text" contains the url to fetch the image
    WinJS.xhr({url:text}).then(function(result){
    //buildImage is a placeholder, I imagine you currently build the image somewhere 
    food.image = buildImage(result);
    });
    // Push it to the List
    this.ingredients.push(food);
    Not entirely sure because I dont know how the Data source was declared but it might work, a bindable data source should fire the one-way data binding update when the properties of the binded items are modified.

    • Marked as answer by Freddixx Thursday, January 17, 2013 3:29 AM
    Tuesday, January 15, 2013 4:55 PM
  • Okay, I didn't have much time yesterday so it took me until now to figure out the best deal for me. Based on your suggestion I rewrote the whole thing to this:

    var user = "Anonymous";
    var image = "/images/preload.gif";
    // Instantiate the object
    var food = new Model.Food(text, user, image);
    // Initiate the image getting process
    ViewModel.buildImage(food).then(function (img) {
        food.image = img;
    });
    // Push it to the List
    ViewModel.addIngredient(food);


    And the method itself is now a promise (my first one.. yeehaaw):

    buildImage: function (food) {
        return new WinJS.Promise(function (complete) {     
            WinJS.xhr({url:"http://api.flickr.com...."
            }).then(function(response){
               .....
            }).then(function (imageSearchResultJSON) { 
                ....
                return WinJS.xhr({ url: "http://api.flickr.com/...."});
            }).then(function (response) {
                ...
                complete(imagePath);
            }).done(null, function (err) {
                // Error 0: The search returned no image.
                if (err == 0)
                    food.image = "/images/nonefound.png";
                return;
            });
        })
    }

    With the whole rewrite the thing is much cleaner now. Thanks again for your help!


    • Marked as answer by Freddixx Thursday, January 17, 2013 3:29 AM
    Thursday, January 17, 2013 3:27 AM

All replies

  • Hi,

    Please refer to my blog : http://blogs.msdn.com/b/win8devsupport/archive/2013/01/10/using-html5-javascript-in-windows-store-apps-data-access-and-storage-mechanism-iii.aspx .

    There has a similar scenario with you.


    Roy
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Tuesday, January 15, 2013 5:42 AM
  • I just read your post and downloaded the code. I don't think your scenario is similiar to mine. You are just reading records from a database. In my case items are added to a ListView and the underlying data changes afterwards as the promise returns the new image path.

    I don't see any indicators of binding and refreshing in your code or am I just blind?

    Tuesday, January 15, 2013 12:57 PM
  • Are you doing the databind with a function renderer or by an HTML template?

    Because if its a function renderer I would try the Placeholder Renderer template:

    var listView = element.querySelector("#myListView").winControl;
    listView.itemTemplate = placeholderRenderer;

    function placeholderRenderer(itemPromise) { // create a basic template for the item which doesn't depend on the data var element = document.createElement("div"); element.className = "itemTempl"; element.innerHTML = "<div class='content'>...</div>"; // return the element as the placeholder, and a callback to update it when data is available return { element: element, renderComplete: itemPromise.then(function (item) { //get the image from Flickr through an async call, maybe WinJS.xhr? WinJS.xhr({url:item.data.image}).then(function(){ // mutate the element to include the data element.querySelector(".content").innerText = item.data.name; element.insertAdjacentHTML("afterBegin", "<img src='" + item.data.image + "' alt='Databound image' />"); }); }) }; }


    Im not sure if WinJS is the best way to get your image from Flickr, but the point is that the call should be an async one.




    • Edited by Ealsur Tuesday, January 15, 2013 4:11 PM Code syntax
    Tuesday, January 15, 2013 4:10 PM
  • Hi Ealsur,

    i am currently using an HTML template:

    <div id="groceryListTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
                        <div class="smallListIconTextItem">
                            <img src="#" class="smallListIconTextItem-Image" data-win-bind="src: image" />
                            <div class="smallListIconTextItem-Detail">
                                <h4 data-win-bind="innerText: name"></h4>
                                <h4 data-win-bind="innerText: user"></h4>
                            </div>
                        </div>
                    </div>  
    
                    <div id="grocerylistView" class="win-selectionstylefilled" data-win-control="WinJS.UI.ListView" 
                        data-win-options="{ 
                        itemDataSource: ViewModel.ingredients.dataSource, 
                        itemTemplate: select('#groceryListTemplate'), 
                        selectionMode: 'none', 
                        tapBehavior: 'none', 
                        swipeBehavior: 'none', 
                        layout: { type: WinJS.UI.GridLayout } 
                    }"></div>

    I would really like to stick to this because I want to decouple the UI from my logic layer. Is there a possibility to do so? And yes: I fetch the image asynchronously via XHR.

    Tuesday, January 15, 2013 4:30 PM
  • Maybe the solution can be achieved as a Promise:

    // Instantiate the object
    var food = new Model.Food(text, user, image);
    //assuming "text" contains the url to fetch the image
    WinJS.xhr({url:text}).then(function(result){
    //buildImage is a placeholder, I imagine you currently build the image somewhere 
    food.image = buildImage(result);
    });
    // Push it to the List
    this.ingredients.push(food);
    Not entirely sure because I dont know how the Data source was declared but it might work, a bindable data source should fire the one-way data binding update when the properties of the binded items are modified.

    • Marked as answer by Freddixx Thursday, January 17, 2013 3:29 AM
    Tuesday, January 15, 2013 4:55 PM
  • Actually it is already like that:

    fetchImage: function (term) { // ... = code omitted // Reserve vars for the search result var imageSearchResultJSON; var imageJSON; // Start the async chain WinJS.xhr({ url: "http://api.flickr.com/services/rest/?method=flickr.photos.search......." }) .then(function (response) { imageSearchResultJSON = JSON.parse(response.responseText); // ... }) .then(function () { var photoID = imageSearchResultJSON["photos"]["photo"][0]["id"]; var photoSecret = imageSearchResultJSON["photos"]["photo"][0]["secret"]; return WinJS.xhr({ url: "http://api.flickr.com/services/rest/?method=flickr.photos.getInfo........"}); }) .then(function (response) { // ... imageJSON = JSON.parse(response.responseText); var imagePath = "http://farm" + ".....jpg"; return imagePath; }) .done(null, function (err) { return error(err); }), function (progress) {}

    }

    I omitted lots of code to keep it short. But the full version definitely works and returns the proper image 

    Tuesday, January 15, 2013 5:09 PM
  • Right, but in your original code:

    // This is a async call, image is still undefined at this point
    var image = ViewModel.fetchImagePath(text);
    // Instantiate the object
    var food = new Model.Food(text, user, image);
    // Push it to the List
    this.ingredients.push(food);

    You are not waiting for the Promise , because the Promise is inside your fetchImagePath function , you need to somehow hook the Promise with the "food" object, so the Promise, when its done, updates directly the object, see that in my previous code I update the "food.image" atribute directly on the WinJS complete handler.

    Tuesday, January 15, 2013 5:16 PM
  • Ah, I see. I thought that the fetchImagePath() method serves as a promise as well. I will put in your code and get back to you asap.

    Btw: Thanks for all your fast help!

    Tuesday, January 15, 2013 5:32 PM
  • Your welcome, fingers crossed!
    Tuesday, January 15, 2013 5:35 PM
  • Okay, I didn't have much time yesterday so it took me until now to figure out the best deal for me. Based on your suggestion I rewrote the whole thing to this:

    var user = "Anonymous";
    var image = "/images/preload.gif";
    // Instantiate the object
    var food = new Model.Food(text, user, image);
    // Initiate the image getting process
    ViewModel.buildImage(food).then(function (img) {
        food.image = img;
    });
    // Push it to the List
    ViewModel.addIngredient(food);


    And the method itself is now a promise (my first one.. yeehaaw):

    buildImage: function (food) {
        return new WinJS.Promise(function (complete) {     
            WinJS.xhr({url:"http://api.flickr.com...."
            }).then(function(response){
               .....
            }).then(function (imageSearchResultJSON) { 
                ....
                return WinJS.xhr({ url: "http://api.flickr.com/...."});
            }).then(function (response) {
                ...
                complete(imagePath);
            }).done(null, function (err) {
                // Error 0: The search returned no image.
                if (err == 0)
                    food.image = "/images/nonefound.png";
                return;
            });
        })
    }

    With the whole rewrite the thing is much cleaner now. Thanks again for your help!


    • Marked as answer by Freddixx Thursday, January 17, 2013 3:29 AM
    Thursday, January 17, 2013 3:27 AM