Question

I'm trying to make an "advanced search" view, using two partial views and Ajax. I defined a "SearchFilter" entity having as properties all the available search criteria. On submit in the "_Filter" partial view (the OnSuccess AjaxOption), I need to pass it to the "ListResults" action which updates the "_Results" partial view.

The problem is that I always get a null entity as incoming parameter of the ListResults action.

The code is as follows:

AdvancedSearchView.cshtml

@model MyApp.ViewModels.SearchFormViewModel
@{
    ViewBag.Title = "Advanced search";
}
    <div id="divFilter">
        @Html.Partial("_Filter", Model)
    </div>
    <div id="divResults">
        @Html.Partial("_Results", Model.ResultsList)
    </div>

_Filter.cshtml

@model MyApp.ViewModels.SearchFormViewModel
<script type="text/javascript">
    function getForm(url, divName) {
        var obj = new Date();
        url = (url.indexOf('?', 0) != -1) ? url + '&uid=' + obj.getTime() : url + '?uid=' + obj.getTime();
        $.get(url, function (data) {
            $("#" + divName).html(data);
        });
    }
</script>

    @using (Ajax.BeginForm("Search", null, new AjaxOptions
    {
        UpdateTargetId = "divFilter",
        InsertionMode = InsertionMode.Replace,
        OnSuccess="getForm('"+Url.Action("ListResults", "Products", new { myFilter = Model.CurrentFilter}) + "','divResults')"
    }, new { id = "idSearchForm" }))
    {
        <fieldset style="width: 800px; line-height: 1.4em;">
            <legend>Configure your search filters</legend>
        ...
        </fieldset>
        <input type="submit" value="Rechercher" class="submit" style="width: 280px" />
    }

Controller

    public ActionResult Search()
    {
SearchFilter currentFilter = new SearchFilter();
List<Product> filteredProductsList = repository.FindProducts_Filtered(currentFilter);
          return View("AdvancedSearchView", new AdvancedSearchFormViewModel(currentFilter, filteredProductsList/* , necessary select lists */));
    }

    [HttpPost]
    public ActionResult Search(AdvancedSearchFormViewModel model)
    {
        SearchFilter currentFilter = model.CurrentFilter;
        // set the necessary select lists

        List<Product> filteredProductsList = repository.FindProducts_Filtered(currentFilter);
        return PartialView("_Filter", AdvancedSearchFormViewModel(currentFilter, filteredProductsList/* , necessary select lists */));
    }

    public ActionResult ListResults(SearchFilter myFilter)
    {
        List<Product> filteredProductsList = repository.FindProducts_Filtered(currentFilter);
        return PartialView("_Results", filteredProductsList);
    }

The view model

public class AdvancedSearchFormViewModel
    {
        // Properties
        public SearchFilter CurrentFilter { get; set; }
        public List<Product> ResultsList { get; set; }
        // some SelectLists

        // Constructor

        public AdvancedSearchFormViewModel()
        {}

        public AdvancedSearchFormViewModel(SearchFilter pCurrentFilter, List<Product> pResultsList, /* necessary select lists*/)
        {
            CurrentFilter = pCurrentFilter;
            ResultsList = pResultsList;
            // the SelectLists
        }
    }

I have no doubt I do something wrong, but I cannot see what it is.

The generated HTML markup of the BeginForm is like this:

<form action="/Products/Search" data-ajax="true" data-ajax-mode="replace" data-ajax-success="getForm(&#39;/Products/ListResults?myFilter=MyApp.Models.SearchFilter&amp;uid=2622ea0e-d7dc-48fa-b65d-519978ee40b3&#39;,&#39;divResults&#39;)" data-ajax-update="#divFilter" id="idSearchForm" method="post">
Was it helpful?

Solution

The reason you are getting a null value for the myFilter argument of the ListResults action is because this is what you are submitting to the server:

/Products/ListResults?myFilter=MyApp.Models.SearchFilter&uid=2622ea0e-d7...

The default modelbinder is trying to turn the string "MyApp.Models.SearchFilter" into an instance of MyApp.Models.SearchFilter, which it cannot do.

I can't see the code for this model object, but you should try sending each of the parameters individually so that the modelbinder is able to construct an instance of the SearchFilter out of its properties:

OnSuccess="getForm('"+Url.Action("ListResults", "Products", new { 
    Prop1 = Model.CurrentFilter.Prop1, 
    Prop2 = Model.CurrentFilter.Prop2, 
    etc...
})...

Update after comment 1

To answer your question about showing parameter values in the URL, this is how HTTP works. If you do not want to show any parameters sent to the server in the URL, then you will need to do an HTTP POST instead of an HTTP GET.

However, any time your operation is idempotent, you should use an HTTP GET. HTTP POST should be reserved for when the input submission should somehow alter the state of the application. In actuality, I disagree with your using [HttpPost] for your public ActionResult Search(AdvancedSearchFormViewModel model) action method. Since all it is doing is returning data to be viewed, it is an idempotent action.

That said, there is nothing preventing you from breaking this guideline and doing an HTTP POST instead of a GET. Personally, I don't see a problem with the URL parameters. You see them all the time on the web. For example, see the URL for this link.

HTTP does not understand complex objects, only text. To send data for complex objects over HTTP, they need to be broken down into their text parts. To send data like this over an HTTP GET, they need to be in the URL.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top