Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>I would recommend you using an <a href="http://msdn.microsoft.com/en-us/library/ee728598.aspx" rel="noreferrer">AsyncController</a> for this task to avoid jeopardizing ASP.NET worker threads which is one the worst thing that might happen to an ASP.NET application => running out of worker threads. It's like running out of fuel in the middle of the desert. You most certainly die.</p> <p>So let's start by writing an extension method that will allow us converting the legacy WebClient event based pattern into the new task based pattern:</p> <pre><code>public static class TaskExtensions { public static Task&lt;string&gt; DownloadStringAsTask(this string url) { var tcs = new TaskCompletionSource&lt;string&gt;(url); var client = new WebClient(); client.DownloadStringCompleted += (sender, args) =&gt; { if (args.Error != null) { tcs.SetException(args.Error); } else { tcs.SetResult(args.Result); } }; client.DownloadStringAsync(new Uri(url)); return tcs.Task; } } </code></pre> <p>Armed with this extension method in hand we could now define a view model that will basically reflect the requirements of our view:</p> <pre><code>public class DownloadResultViewModel { public string Url { get; set; } public int WordCount { get; set; } public string Error { get; set; } } </code></pre> <p>Then we move on to an asyncrhonous controller that will contain 2 actions: a standard synchronous <code>Index</code> action that will render the search form and an asynchronous <code>Search</code> action that will perform the actual work: </p> <pre><code>public class HomeController : AsyncController { public ActionResult Index() { return View(); } [AsyncTimeout(600000)] [HttpPost] public void SearchAsync(string searchText) { AsyncManager.Parameters["searchText"] = searchText; string[] urls = { "http://www.msnbc.com", "http://www.yahoo.com", "http://www.nytimes.com", "http://www.washingtonpost.com", "http://www.latimes.com", "http://www.unexistentdomainthatwillcrash.com", "http://www.newsday.com" }; var tasks = urls.Select(url =&gt; url.DownloadStringAsTask()); AsyncManager.OutstandingOperations.Increment(urls.Length); Task.Factory.ContinueWhenAll(tasks.ToArray(), allTasks =&gt; { var results = from task in allTasks let error = task.IsFaulted ? task.Exception.Message : null let result = !task.IsFaulted ? task.Result : string.Empty select new DownloadResultViewModel { Url = (string)task.AsyncState, Error = error, WordCount = result.Split(' ') .Where(x =&gt; string.Equals(x, searchText, StringComparison.OrdinalIgnoreCase)) .Count() }; AsyncManager.Parameters["results"] = results; AsyncManager.OutstandingOperations.Decrement(urls.Length); }); } public ActionResult SearchCompleted(IEnumerable&lt;DownloadResultViewModel&gt; results) { return View("index", results); } } </code></pre> <p>Now we define an <code>~/Views/Home/Index.cshtml</code> view that will contain the search logic as well as the results:</p> <pre><code>@model IEnumerable&lt;DownloadResultViewModel&gt; @using (Html.BeginForm("search", null, new { searchText = "politics" })) { &lt;button type="submit"&gt;Search&lt;/button&gt; } @if (Model != null) { &lt;h3&gt;Search results&lt;/h3&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Url&lt;/th&gt; &lt;th&gt;Word count&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; @Html.DisplayForModel() &lt;/tbody&gt; &lt;/table&gt; } </code></pre> <p>And of course the corresponding display template that will be rendered automatically for each element of our model (<code>~/Views/Shared/DisplayTemplates/DownloadResultViewModel.cshtml</code>):</p> <pre><code>@model DownloadResultViewModel &lt;tr&gt; &lt;td&gt;@Html.DisplayFor(x =&gt; x.Url)&lt;/td&gt; &lt;td&gt; @if (Model.Error != null) { @Html.DisplayFor(x =&gt; x.Error) } else { @Html.DisplayFor(x =&gt; x.WordCount) } &lt;/td&gt; &lt;/tr&gt; </code></pre> <p>Now, since the search operation could take quite a long time your users could quickly get bored without being able to use some of the other hundredths of functionalities that your webpage has to offer them.</p> <p>In this case it is absolutely trivial to invoke the <code>Search</code> controller action using an AJAX request and showing a spinner to inform the users that their search is in progress but without freezing the webpage allowing them to do other things (without navigating away from the page obviously).</p> <p>So let's do that, shall we?</p> <p>We start by externalizing the results into a partial (<code>~/Views/Home/_Results.cshtml</code>) without touching at the display template:</p> <pre><code>@model IEnumerable&lt;DownloadResultViewModel&gt; @if (Model != null) { &lt;h3&gt;Search results&lt;/h3&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Url&lt;/th&gt; &lt;th&gt;Word count&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; @Html.DisplayForModel() &lt;/tbody&gt; &lt;/table&gt; } </code></pre> <p>and we adapt our <code>~/Views/Home/Index.cshtml</code> view to use this partial:</p> <pre><code>@model IEnumerable&lt;DownloadResultViewModel&gt; @using (Html.BeginForm("search", null, new { searchText = "politics" })) { &lt;button type="submit"&gt;Search&lt;/button&gt; } &lt;div id="results"&gt; @Html.Partial("_Results") &lt;/div&gt; </code></pre> <p>and of course the <code>SearchCompleted</code> controller action that must now return only the partial result:</p> <pre><code>public ActionResult SearchCompleted(IEnumerable&lt;DownloadResultViewModel&gt; results) { return PartialView("_Results", results); } </code></pre> <p>Now all that's left is to write a simple javascript that will AJAXify our search form. So this could happen into a separate js that will reference in our layout:</p> <pre><code>$(function () { $('form').submit(function () { $.ajax({ url: this.action, type: this.method, success: function (results) { $('#results').html(results); } }); return false; }); }); </code></pre> <p>Depending on whether you referenced this script in the <code>&lt;head&gt;</code> section or at the end of the body you might not need to wrap it in a <code>document.ready</code>. If the script is at the end you could remove the wrapping document.ready function from my example.</p> <p>And the last part is to give some visual indication to the user that the site is actually performing a search. This could be done using a <a href="http://api.jquery.com/category/ajax/global-ajax-event-handlers/" rel="noreferrer">global ajax event handler</a> that we might subscribe to:</p> <pre><code>$(function () { $(document).ajaxStart(function () { $('#results').html('searching ...'); }); }); </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload