views:

406

answers:

3

I have a SearchController with an action that can execute some long running searches and return a results page. The searches can take anywhere from 1 to 60 seconds. The URL for a search is an HTTP GET request of the form: http://localhost/Search?my=query&is=fancy

The experience I'm looking for is similar to the many travel sites that are out there. I would like to show an intermediate "Loading..." page where, ideally:

  1. The user can reload the page without restarting the search
  2. Once the back-end search is finished, the user is redirected to the results
  3. The experience degrades for browsers with JavaScript disabled
  4. The back button / browser history should not include this interstitial page.
  5. In the case of a short search (1 second), it doesn't have a significant impact on either the time to get to the results OR the experience (significantly ugly page flashes, whatever)

Those are nice-to-have's. I'm open to all ideas! Thanks.

+1  A: 

You could do it the following way:

  • do the search request (the GET url) with AJAX
  • the search url doesn't return the results, but returns some Json or XML content with the URL of the actual results
  • the client page shows a "loading..." message while waiting for the AJAX call to finish
  • the client page redirects to the results page when finished.

An example using jquery:

<div id="loading" style="display: none">
  Loading...
</div>
<a href="javascript:void(0);" 
  onclick="searchFor('something')">Search for something</a>

<script type="text/javascript">
  function searchFor(what) {
    $('#loading').fadeIn();
    $.ajax({ 
      type: 'GET', 
      url: 'search?query=' + what, 
      success: function(data) { 
        location.href = data.ResultsUrl; 
      } 
    });        
  }
</script>

(edit:)

The controller would be something like:

public class SearchController 
{
  public ActionResult Query(string q) 
  {
    Session("searchresults") = performSearch();
    return Json(new { ResultsUrl = 'Results'});
  }

  public ActionResult Results()
  {
    return View(Session("searchresults"));
  }
}

Consider it pseudo-code: i did not actually test it.

Jan Willem B
+1  A: 

In order to keep it javascript-less, you can break the search into multiple actions.

The first action (/Search/?q=whodunit) just does some validation of your parameters (so you know if you need to re-display the form) and then returns a view which uses a meta-refresh to point the browser back to the "real" search action.

You can implement this with two separate controller actions (say Search and Results):

public ActionResult Search(string q)
{
    if (Validate(q))
    {
        string resultsUrl = Url.Action("Results", new { q = q });
        return View("ResultsLoading", new ResultsLoadingModel(resultsUrl));
    }
    else
    {
        return ShowSearchForm(...);
    }
}

bool Validate(string q)
{
    // Validate
}

public ActionResult Results(string q)
{
    if (Validate(q))
    {
        // Do Search and return View
    }
    else
    {
        return ShowSearchForm(...);
    }
}

But this gives you some snags as far as refreshing goes. So you can re-merge them back into a single action which can signal itself of the two-phase process using TempData.

static string SearchLoadingPageSentKey = "Look at me, I'm a magic string!";

public ActionResult Search(string q)
{
    if (Validate(q))
    {
        if (TempData[SearchLoadingPageSentKey]==null)
        {
            TempData[SearchLoadingPageSentKey] = true;
            string resultsUrl = Url.Action("Search", new { q = q });
            return View("ResultsLoading", new ResultsLoadingModel(resultsUrl));
        }
        else
        {
            // Do actual search here
            return View("SearchResults", model);
        }
    }
    else
    {
        return ShowSearchForm(...);
    }
}

This covers points 2, 3, 4 and arguably 5.

To include support for #1 implies that you're going to store the results of the search either in session, db, etc..

In this case, just add your desired cache implementatin as part of the "Do actual search here" bit, and add a check-for-cached-result to bypass the loading page. e.g.

if (TempData[SearchLoadingPageSentKey]==null)

becomes

if (TempData[SearchLeadingPageSentKey]==null && !SearchCache.ContainsKey(q))

daveidmx
+1  A: 

Good question. I may soon have to implement a similar solution in asp.net mvc myself, but I don't think it would require a fundamentally different implementation than a webforms-based solution, of which there are various examples on the net:

I've previously built an implementation based on the first link above with web forms. The basic process is:

  1. Initial page is requested w/ search parameters
  2. This page starts up a new thread which performs the long-running task
  3. This page redirects user to an "under process" page which has a http refresh header set to reload every couple seconds
  4. The thread performing the search updates a "global" static search-progress object indicating the % complete, and the under process page reads from it, displaying the progress. (each search is stored by a GUID id in a Hashtable, so multiple concurrent searches are supported)
  5. Once complete the thread updates the search-progress as such and when the under process page detects this, it redirects to a final "result" page.

(Definitely check the second link from MSDN, it's quite different solution, but not one I've only skimmed over.)

The benefit of this is that it doesn't require Javascript at all. The biggest drawback that I can think of (from a user's perspective) is that it's not entirely "Web 2.0" and users will have to wait through a series of browser refreshes.

Something based on @Jan Willem B's AJAX-based suggestion should be a viable alternative to this multi-threaded wait state pattern. Which best meets your requirements is something you'll have to decide on your own. The example from aspfree.com I posted should meet a majority of your requirements though and work just as well with MVC as Web forms.

Kurt Schindler