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))