How to design an MVC5 Global Search feature in Lay

2019-04-15 05:54发布

问题:

I am trying to implement a "global search" feature, that is available above our main menu, in all Views within our application. It looks like this:

The "global search" is a jQuery autocomplete input field. It resides in our _Layout.cshtml, which is a shared View and gets loaded many times by other views. Essentially it will display an auto-suggestion list for search keywords. Our list of keyword suggestions is roughly 6000 items.

Our HomeController looks like this:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Home()
    {
        ViewBag.Message = " Home Page.";

        GlobalSearchController controller = new GlobalSearchController();
        //_Layout.cshtml uses this for the auto-complete jQuery list
        var suggestions = controller.GetGlobalSearchSuggestions(); 
        return View(suggestions);
    }

    public ActionResult SearchResults()
    {
        ViewBag.Message = "Search Results page.";

        GlobalSearchController controller = new GlobalSearchController();
        var searchKeyword = "technology";
        //SearchResults.html uses this for the search results data
        var results = controller.GetGlobalSearchResults(searchKeyword); 
        ViewBag.SearchKeyword = searchKeyword;
        return View(results);
    }
}

_Layout.cshtml uses this model:

@model MyApplication.Models.GlobalSearchSuggestions

SearchResults.cshtml uses this model:

@model IQueryable<MyApplication.Models.GlobalSearchResult>  

My problem begins when I use the @model declarative in _Layout.cshtml.

I get an error like this:

Message = "The model item passed into the dictionary is of type 'System.Web.Mvc.HandleErrorInfo', but this dictionary requires a model item of type 'MyApplication.Models.GlobalSearchSuggestions'."

If I remove the model declarative for _Layout.cshtml, and retrieve the "suggestions" through another means (like AJAX), it will allow the SearchResults.cshtml to work. No error is produced. But I would much rather use the model instead of AJAX. So, if I leave the model declarative in the _Layout.cshtml, I get the exception.

I also cannot load the "suggestions" from any View other than Home. Why is that? If I go to another view within our application, and I try to perform a "global search" from our _Layout.cshtml widget, I do not get any "suggestions" or data in the jQuery autocomplete. Why does it only work for the Home view and Home controller??

How do I avoid this exception and use both @model declaratives? And how can I get _Layout.cshtml to consistently display suggestions in the auto-complete field (and not just from the Home page?)?

Any help is appreciated. Thank you!

回答1:

This sounds like a good use case for Child Actions.

This is a basic example with AJAX so the user will see results without a page reload.

_Layout.cshtml

<div class="header">
    @Html.Action("SearchWidget", "GlobalSearch")
</div>

@RenderBody()

<script src="jquery.js" />
<script>
    $(".global-search-form").on("click", "button", function(e)
    {
        $.ajax({
            url: "/GlobalSearch/Search",
            method: "GET",
            data: { item: $("input[name='item']").val() }
        })
        .then(function(result)
        {
            $(".global-search-result").html(result);
        });
    });
</script>

_Search.cshtml

<div class="global-search-widget">
    <div class="globa-search-form">
        <label for="item">Search For:</label>
        <input type="text" name="item" value="" />
        <button type="button">Search</button>
    </div>
    <div class="global-search-results"></div>
</div>

_SearchResults.cshtml

@model MyNamespace.SearchResults

<div>Results</div>
<ul>
@foreach(var item in Model.Suggestions)
{
    <li>@item</li>
}
</ul>

SearchResults

public class SearchResults
{
    public List<string> Suggestions { get; set; }
}

GlobalSearchController

[HttpGet]
[ChildActionOnly]
public ActionResult SearchWidget()
{
    return PartialView("_Search");
}

[HttpGet]
public ActionResult Search(string item)
{
    SearchResults results = searchService.Find(item);
    return PartialView("_SearchResults", results);
}

We keep the @model declaration out of the Layout page and move it to the Child Action's partial view. This example loaded the search widget into Layout but you can use it on any view you want.

To keep things simple here, the AJAX is triggered by a button but you can modify it to trigger on a delayed text change. The result could also be JSON instead of a parital view -- some client-side Type-Ahead plug-ins may handle the results as JSON.

If you want to navigate to a results page

You can drop all the script and convert your widget to a proper form.

@model MyNamespace.SearchForm

@using(Html.BeginForm("Search", "GlobalSearch", FormMethod.Get, new { item = ViewBag.GlobalSearchKey })
{
    @Html.TextBoxFor(m => m.Item)
    <button type="submit">Search</button>
}

A search model

public class SearchForm
{
    public string Item { get; set; }
}

Adjust your layout to pass a parameter back to the search widget. This will maintain the search key in the results page.

@Html.Action("SearchWidget", "GlobalSearch", new { item = ViewBag.GlobalSearchKey })

The SearchWidget action now passes a parameter to populate the form (if provided).

[HttpGet]
[ChildActionOnly]
public ActionResult SearchWidget(string item)
{
    var model = new SearchForm
    {
        Item = item ?? ""
    };
    return PartialView("_Search", model);
}

[HttpGet]
public ActionResult Search(SearchForm model)
{
    var results = searchService.Find(model.Item);
    ViewBag.GlobalSearchKey = model.Item;  // keep the same value for the form

    return View("SearchResults", results);  // full view with layout
}

We use the ViewBag for the search key so any action using the layout will not have to define a common model.