1

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: enter image description here

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!

AussieJoe
  • 1,285
  • 1
  • 14
  • 29
  • This sounds like a good use case for [Child Actions](http://stackoverflow.com/questions/12530016/what-is-an-mvc-child-action). Also, a model is not typically declared on a _Layout page as it forces all your views to use that model which is why you are seeing these errors. – Jasen May 18 '16 at 19:46
  • @Jasen thanks for the suggestion! I tried using @Html.Action("SearchSuggestions", "Home") and added a new View called "SearchSuggestions", but unfortunately, it just endlessly loops when loading the _Layout.cshtml file. Any idea how to remove the model from _Layout.cshtml? And where to put this global search "widget"? – AussieJoe May 18 '16 at 20:14
  • 2
    Make sure your child actions `return PartialView("view", model)` so you don't reload the _Layout recursively. You can use a global search child action on any view -- the _Layout is appropriate if you want it on every page. – Jasen May 18 '16 at 22:04
  • 2
    I also don't think you can remove AJAX from the solution. You'll want to show results _without_ causing a page reload for the user. – Jasen May 18 '16 at 22:12

1 Answers1

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.

Community
  • 1
  • 1
Jasen
  • 14,030
  • 3
  • 51
  • 68
  • Instead of putting the results in the div "global-search-result", is there a way to just return the SearchResults view entirely? I am trying it and it passes but stays on the Home view. I tried to assign "results" to $("#body").html but that throws a jQuery warning and it does not render our main menu at all (no dropdown or animation). – AussieJoe May 20 '16 at 20:03
  • 1
    It's meant to stay on the same main page. You'll need to return a redirect if you want a completely different page. – Jasen May 20 '16 at 20:07
  • Can I redirect and pass the model? I only see options for URL? – AussieJoe May 20 '16 at 20:10
  • You can either redirect, but you'll need to store the results in `TempData` or `Session` then retrieve it in the redirected action. Or you can return a full view directly with the model. – Jasen May 20 '16 at 20:27
  • What's the purpose of using a RedirectToAction ? If I am already in an ActionResult method, why the need to jump elsewhere? Can you edit your answer to accomplish this? I feel this is close to being solved :) – AussieJoe May 20 '16 at 20:33
  • A lot of this depends on your UI/UX and what URL you may want to present to the user. – Jasen May 20 '16 at 20:39
  • URL doesnt matter too much, as long as the widget loads the application body. Right now all the code steps through fine but it never redirects or loads the body with search results. – AussieJoe May 20 '16 at 20:48
  • Currently the url is: /Search/SearchResults – AussieJoe May 20 '16 at 21:03
  • if I add a hyperlink it works, like so: Search Results The AJAX url does not. What would be the different approach? – AussieJoe May 23 '16 at 14:45
  • stupid me, this worked!! document.location.href = 'Search/SearchResults?searchKeyword=' + keywordSelected; – AussieJoe May 23 '16 at 15:06
  • HI there, can someone tell me how to build the find method of the searchService Class in the Global Search Controller. I'm trying to implement this solution as well. – Khanyisa Fante Dec 30 '18 at 11:46