1

基本上我想实现简单的搜索功能,每当用户在视图中的文本框中输入一些关键字并单击提交按钮时,我想使用 TPL 异步机制对预定义的网站 url 进行 ASYNC 调用。当我对控制台应用程序执行相同操作时,它就像一个魅力,但不适用于 ASP.NET MVC3。

我找不到原因

 public ActionResult Index()
    {
        ViewBag.Message = "Welcome to ASP.NET MVC!";

        return View();
    }

    public ActionResult About()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Index(string text)
    {
        string[] url =  { "http://www.msnbc.com", "http://www.yahoo.com",
                             "http://www.nytimes.com", "http://www.washingtonpost.com",
                             "http://www.latimes.com", "http://www.newsday.com" };
        Task<string[]> webTask = this.GetWordCounts(url, text);
        string[] results = null;
        try
        {
            results = webTask.Result;
        }
        catch (AggregateException e)
        {

        }

        return View("Index", results);

    }

    //Taken from MSDN
    Task<string[]> GetWordCounts(string[] urls, string name)
    {
        TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>();
        WebClient[] webClients = new WebClient[urls.Length];

        object m_lock = new object();
        int count = 0;
        List<string> results = new List<string>();
        for (int i = 0; i < urls.Length; i++)
        {
            webClients[i] = new WebClient();

            #region callback
            // Specify the callback for the DownloadStringCompleted
            // event that will be raised by this WebClient instance.
            webClients[i].DownloadStringCompleted += (obj, args) =>
            {
                if (args.Cancelled == true)
                {
                    tcs.TrySetCanceled();
                    return;
                }
                else if (args.Error != null)
                {
                    // Pass through to the underlying Task
                    // any exceptions thrown by the WebClient
                    // during the asynchronous operation.
                    tcs.TrySetException(args.Error);
                    return;
                }
                else
                {
                    // Split the string into an array of words,
                    // then count the number of elements that match
                    // the search term.
                    string[] words = null;
                    words = args.Result.Split(' ');
                    string NAME = name.ToUpper();
                    int nameCount = (from word in words.AsParallel()
                                                     where word.ToUpper().Contains(NAME)
                                                     select word)
                                                    .Count();

                    // Associate the results with the url, and add new string to the array that 
                    // the underlying Task object will return in its Result property.
                    results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, name));
                }

                // If this is the last async operation to complete,
                // then set the Result property on the underlying Task.
                lock (m_lock)
                {
                    count++;
                    if (count == urls.Length)
                    {
                        tcs.TrySetResult(results.ToArray());
                    }
                }
            };
            #endregion

            // Call DownloadStringAsync for each URL.
            Uri address = null;
            try
            {
                address = new Uri(urls[i]);
                // Pass the address, and also use it for the userToken 
                // to identify the page when the delegate is invoked.
                webClients[i].DownloadStringAsync(address, address);
            }

            catch (UriFormatException ex)
            {
                // Abandon the entire operation if one url is malformed.
                // Other actions are possible here.
                tcs.TrySetException(ex);
                return tcs.Task;
            }
        }

        // Return the underlying Task. The client code
        // waits on the Result property, and handles exceptions
        // in the try-catch block there.
        return tcs.Task;
    }

这是我的观点 - 现在我将关键字硬编码为 microsoft

@using (Html.BeginForm("Index", "Home", new { text = "Microsoft" }))
{
<input type="submit" />
}

更新:它永远停留在 Index Post 方法的 try 块内

4

1 回答 1

9

我建议您为此任务使用AsyncController以避免危害 ASP.NET 工作线程,这是 ASP.NET 应用程序可能发生的最糟糕的事情之一 => 工作线程用完。这就像在沙漠中耗尽燃料。你肯定会死。

因此,让我们从编写一个扩展方法开始,它允许我们将基于 WebClient 事件的旧模式转换为基于任务的新模式:

public static class TaskExtensions
{
    public static Task<string> DownloadStringAsTask(this string url)
    {
        var tcs = new TaskCompletionSource<string>(url);
        var client = new WebClient();
        client.DownloadStringCompleted += (sender, args) =>
        {
            if (args.Error != null)
            {
                tcs.SetException(args.Error);
            }
            else
            {
                tcs.SetResult(args.Result);
            }
        };
        client.DownloadStringAsync(new Uri(url));
        return tcs.Task;
    }
}

有了这个扩展方法,我们现在可以定义一个基本上反映我们视图要求的视图模型:

public class DownloadResultViewModel
{
    public string Url { get; set; }
    public int WordCount { get; set; }
    public string Error { get; set; }
}

然后我们转到一个包含 2 个操作的异步控制器:一个标准的同步Index操作,它将呈现搜索表单,一个异步Search操作,将执行实际工作:

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 => url.DownloadStringAsTask());
        AsyncManager.OutstandingOperations.Increment(urls.Length);
        Task.Factory.ContinueWhenAll(tasks.ToArray(), allTasks => 
        {
            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 => string.Equals(x, searchText, StringComparison.OrdinalIgnoreCase))
                        .Count()
                };
            AsyncManager.Parameters["results"] = results;
            AsyncManager.OutstandingOperations.Decrement(urls.Length);
        });
    }

    public ActionResult SearchCompleted(IEnumerable<DownloadResultViewModel> results)
    {
        return View("index", results);
    }
}

现在我们定义一个~/Views/Home/Index.cshtml包含搜索逻辑和结果的视图:

@model IEnumerable<DownloadResultViewModel>

@using (Html.BeginForm("search", null, new { searchText = "politics" }))
{
    <button type="submit">Search</button>
}

@if (Model != null)
{
    <h3>Search results</h3>
    <table>
        <thead>
            <tr>
                <th>Url</th>
                <th>Word count</th>
            </tr>
        </thead>
        <tbody>
            @Html.DisplayForModel()
        </tbody>
    </table>
}

当然,我们模型的每个元素都会自动呈现相应的显示模板 ( ~/Views/Shared/DisplayTemplates/DownloadResultViewModel.cshtml):

@model DownloadResultViewModel
<tr>
    <td>@Html.DisplayFor(x => x.Url)</td>
    <td>
        @if (Model.Error != null)
        {
            @Html.DisplayFor(x => x.Error)
        }
        else
        {
            @Html.DisplayFor(x => x.WordCount)
        }
    </td>
</tr>

现在,由于搜索操作可能需要相当长的时间,您的用户可能很快就会感到无聊,而无法使用您的网页必须提供的其他百分之几的功能。

Search在这种情况下,使用 AJAX 请求调用控制器操作并显示一个微调器以通知用户他们的搜索正在进行中但没有冻结网页允许他们做其他事情(没有明显地离开页面)是绝对微不足道的.

所以让我们这样做,好吗?

我们首先将结果外部化为部分 ( ~/Views/Home/_Results.cshtml) 而不触及显示模板:

@model IEnumerable<DownloadResultViewModel>
@if (Model != null)
{
    <h3>Search results</h3>
    <table>
        <thead>
            <tr>
                <th>Url</th>
                <th>Word count</th>
            </tr>
        </thead>
        <tbody>
            @Html.DisplayForModel()
        </tbody>
    </table>
}

我们调整我们的~/Views/Home/Index.cshtml观点来使用这个部分:

@model IEnumerable<DownloadResultViewModel>

@using (Html.BeginForm("search", null, new { searchText = "politics" }))
{
    <button type="submit">Search</button>
}

<div id="results">
    @Html.Partial("_Results")
</div>

当然SearchCompleted,现在必须只返回部分结果的控制器操作:

public ActionResult SearchCompleted(IEnumerable<DownloadResultViewModel> results)
{
    return PartialView("_Results", results);
}

现在剩下的就是编写一个简单的 javascript 来 AJAXify 我们的搜索表单。所以这可能发生在一个单独的 js 中,它将在我们的布局中引用:

$(function () {
    $('form').submit(function () {
        $.ajax({
            url: this.action,
            type: this.method,
            success: function (results) {
                $('#results').html(results);
            }
        });
        return false;
    });
});

根据您是在该<head>部分中还是在正文的末尾引用了此脚本,您可能不需要将其包装在document.ready. 如果脚本在最后,您可以从我的示例中删除包装 document.ready 函数。

最后一部分是给用户一些视觉指示,表明该站点实际上正在执行搜索。这可以使用我们可能订阅的全局 ajax 事件处理程序来完成:

$(function () {
    $(document).ajaxStart(function () {
        $('#results').html('searching ...');
    });
});
于 2012-07-09T17:03:21.960 回答