Monday, July 2, 2012

Javascript Metro App Search with Web Services

If you are here, it probably means you are not contented with the default search implementation in javascript metro apps. The default search method when you add a search contract is as below

   1: // This function populates a WinJS.Binding.List with search results for the
   2:         // provided query.
   3:         searchData: function (queryText) {
   4:             var originalResults;
   5:             var regex;
   6:             // TODO: Perform the appropriate search on your data.
   7:             if (window.Data) {
   8:                 originalResults = Data.items.createFiltered(function (item) {
   9:                     regex = new RegExp(queryText, "gi");
  10:                     return (item.title.match(regex) || item.subtitle.match(regex) || item.description.match(regex));
  11:                 });
  12:             } else {
  13:                 originalResults = new WinJS.Binding.List();
  14:             }
  15:             return originalResults;
  16:         },




Pay attention to line number 8. The way the search results are populated is by creating a filtered list of Data.items that match the query text. Which means, you should already have items loaded into the app to be able to search from them. But what if, I do not load all the items in the app but want to search for items in my database that I haven’t loaded?


If you haven’t guessed already, that is exactly what this post will show. Be aware that this is not the only implementation that is possible for this. This is just the way I did it. I really did not see any help or a post that shows this, so I thought I will blog about it, just in case somebody is looking for something similar. And just so you know, this was built using Windows 8 Release Preview, so it could be a bit different if you are on Windows 8 CP.


When you add a search contract to your project, there are two methods in searchResults.js that are of interest to us. They are  handleQuery and searchData. You will see that the handleQuery in turn calls searchData and a couple of other methods. The search data method is synchronous because it is only filtering items that have already been fetched. If you want to call a web service when the user searches, then this method has to be asynchronous since all web service calls made using WinJS.Xhr are asynchronous by default.


I am going to add a new method in data.js called searchEventHandler



   1: function searchEventHandler(data) {
   2:     var searchList = new WinJS.Binding.List();
   3:     return new WinJS.Promise(function (c, e, p) {
   4:         WinJS.xhr({ type: "POST", url: localSettings.values["webserviceUri"], headers: { "Content-type": "application/x-www-form-urlencoded" }, data: data }).done(
   5:             function (result) {
   6:                 if (result != null && result.responseText != "") {
   7:                     var jsonData = JSON.parse(result.responseText);
   8:                     for (var i = 0; i < jsonData.length; i++) {
   9:                         //process your results if need be
  10:                         searchList.push(jsonData[i]);
  11:                     }
  12:                     c(searchList);
  13:                 }
  14:             },
  15:             function error(result) {
  16:                 if (result.responseText != "") {
  17:                     var item = new Object();
  18:                     item.title = "Error occurred.";
  19:                     item.group = sampleGroups[0];
  20:                     item.backgroundImage = lightGray;
  21:                     searchList.push(item);
  22:                     e(searchList);
  23:                 }
  24:  
  25:             }
  26:         );
  27:     });



Will take a moment to explain what this method does. It calls the web service passing the query string as a parameter and adds the results to a WinJS binding list. To make this method asynchronous, I have used WinJS.Promise. The method then calls the success or the error call back with the search results as a parameter. With this, we now have an asynchronous method that calls a web service and returns the search results. We now have to call this method from searchResults.js. Add this method to the data namespace, so it can be called from searchResults.



   1: WinJS.Namespace.define("data", {
   2:     items: groupedItems,
   3:     products:productsList,
   4:     groups: groupedItems.groups,
   5:     getItemsFromGroup: getItemsFromGroup,
   6:     searchEventHandler: searchEventHandler
   7: });

The searchData function then needs to be modified as below



   1: // This function populates a WinJS.Binding.List with search results for the
   2:         // provided query.
   3:         searchData: function (queryText) {
   4:             var originalResults;
   5:             //set the criteria to en-us and on-demand
   6:             var criteria = "culture=en-us&eventType=3&kwdAny=" + queryText;
   7:             data.searchEventHandler(criteria).done(function (results) {
   8:                 originalResults = results;
   9:                 searchResults.generateFilters(originalResults);
  10:                 searchResults.populateFilterBar(currentElement, originalResults);
  11:             });
  12:         }



Apart from the obvious change of calling the searchEventHandler, also note that the generateFilters and the populateFilterBar functions are being called from here instead of handleQuery. This is because both those functions depend on the results. I am leaving out more obvious code changes here like making currentElement a class variable.


One last thing that is left to do is to generate the filters. If the filters are generated from the results, then the generateFilters method has to be changed to update your filters after the search is done rather than before. Since it depends on your implementation of the filters, I am not including that code here.


As I stated in the post earlier, there are innumerable variations through which this can be achieved. Please feel free to let me know if you have done this any differently.


Technorati Tags: ,,,

5 comments:

  1. Hi, I want have question about :

    app activation from search contract (when app not running)

    and also when in snapped view, and then search on app

    because when I try on that 2 condition my app closed

    On normal condition (app already running) search can work fine

    I have implement onActivate listener, but maybe I'm wrong implemented it so not work..

    Do you have example code about this?

    Thanks

    ReplyDelete
    Replies
    1. Hi Erik,

      As long as you have implemented the search contract, the search activate event should be called. While I don't have a code of my own, take a look at the channel 9 video http://channel9.msdn.com/events/BUILD/BUILD2011/APP-406T
      Hopefully this helps you. If not, please feel free to ping me back and we can work it out. For snapped view, make sure your css supports your snapped view and the code checks for snapped view.

      Delete
    2. Hi Vaideeswaran,

      Thanks for your response

      I'm already fix search on snapped view, but

      I'm still have problem with activation from search contract if my app not already running
      It only show blank after splashscreen, and after I enter the query search my app closed
      Which part of my code that I must concern?
      Is that the activated listener?
      My code for this is rather same from the template of search contract

      Thanks,
      Eric

      Delete
    3. Eric,

      Can you confirm the following for me? Did you add code in default.js to add the search event? If not, then this is what you have to do. In default.js, there is a function called app.addEventListener. Make sure your code looks like the below

      app.addEventListener("activated", function (args) {
      if (args.detail.kind === activation.ActivationKind.launch) {
      if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
      // TODO: This application has been newly launched. Initialize
      // your application here.
      }
      else {
      // TODO: This application has been reactivated from suspension.
      // Restore application state here.
      }

      if (app.sessionState.history) {
      nav.history = app.sessionState.history;
      }
      args.setPromise(WinJS.UI.processAll().then(function () {
      if (nav.location) {
      nav.history.current.initialPlaceholder = true;
      return nav.navigate(nav.location, nav.state);
      } else {
      return nav.navigate(Application.navigator.home);
      }
      }));
      }
      else if (args.detail.kind === activation.ActivationKind.search) {
      pageParameters = { queryText: eventObject.detail.queryText };
      eventObject.setPromise(ui.processAll().then(function () {
      return nav.navigate(activation.ActivationKind.search, pageParameters);
      }));
      };
      });

      Note that I have added an else if to have a search event captured. If its a search event then I invoke the search screen. If you have already done this, let me know and we will take this on email

      Delete
    4. Hi Vaideeswaran,

      Thanks for your help.
      I can resolve the problem, so when activated from search I must wait until DOM home.html loaded then navigate to search.html

      Delete