6

What is the best method to retain the results of a form post (view model) across search results page?

I have a search form that contains checkboxes. This form is build up using a view model like

public class SearchViewModel
{
    public string Name { get; set; }
    public string[] Colors { get; set; }
}

When this view model gets posted back I use the values to build a query (using EF). The results are turned into a PagedList.

    public class SearchController : Controller
    {
    public ActionResult Index()
    {
        //this displays the search form.
        return View();
    }

    public ActionResult Results(string game, SearchViewModel vm)
    {
        //this displays the results
        ViewBag.SearchViewModel = vm;
        var matches = _repository.AsQueryable()
            .ColorOr(vm.Colors)
            .WhereIf(vm.Name.IsNotEmpty(), x => x.Name.Contains(vm.Name.Trim()));

            return View(matches.ToPagedList(1, 10));
    }
}

Now that the results are displayed I would like to use Html.PagedListPager and RouteValueDictionary to create paging.

@Html.PagedListPager((IPagedList)Model, page => Url.Action("Results", new RouteValueDictionary(ViewBag.SearchViewModel)))

However; the URL created looks like this:

http://localhost:5139/search?Name=test&Colors=System.String[]&PageIndex=0

The values for Colors ends up being the type not the values. I was hoping the URL looks more like:

 http://localhost:5139/search?Name=test&Colors=[Blue,Pink,Yellow]&PageIndex=0
  1. What is the best method to retain the results of a form post (view model) across search results page?
  2. Can RouteValueDictionary support complex objects?
  3. Should I use something like unbinder
  4. Would I be better off using ViewData or Session?
detroitpro
  • 3,853
  • 4
  • 35
  • 64

2 Answers2

2

What I've done for cases like this, which I find simple, yet powerful, is serialized my view model object to JSON (in your case SearchViewModel), using something like NewtonSoft JSON.net then with the resulting JSON string, do a simple compression of the string via the zlib.net Zlib.DeflateStream class (you could also use something like AES Rijndael but will no doubt be slower and you want speed first and foremost) and then pass the resulting Base64 string across your QueryString.

Then when you are ready to use it again (it's effectively a viewstate), simply decompress the JSON string and deserialize it from JSON into the respective .NET object (again in your case SearchViewModel).

Worked a treat for me, and you don't end up with a URL that is unmanageable or any real measurable performance impacts that I've seen with only a handful of form fields being serialized.

I will elaborate with a code sample soon.

UPDATE: Code samples...

This is what I would do in your particular scenario:

In Results(string, SearchViewModel) action:

public ActionResult Results(string encryptedUrlViewModel, string game, SearchViewModel vm)
{
    SearchViewModel searchUrlViewModel = null;
    if (!string.IsNullOrEmpty(searchUrl)) {
      // only first submission, no url view model set yet, so compress it and store..
      encryptedUrlViewModel = Convert.ToBase64String(
        DeflateStream.CompressString(JsonConvert.SerializeObject(vm)));
      ViewBag.EncryptedUrlViewModel = encryptedUrlViewModel;
    }
    else {
      var jsonUrlViewModel = DeflateStream.UncompressString(Convert.FromBase64String(encryptedUrlViewModel));
      searchUrlViewModel = JsonConvert.DeserializeObject(jsonUrlViewModel, typeof(SearchViewModel)) as SearchViewModel;
      // at this point you should have a serialized 'SearchViewModel' object 
      // ready to use which you can then tweak your query below with.
    }
    var matches = _repository.AsQueryable()
        .ColorOr(vm.Colors)
        .WhereIf(vm.Name.IsNotEmpty(), x => x.Name.Contains(vm.Name.Trim()));

    return View(matches.ToPagedList(1, 10));
}

In view:

@Html.PagedListPager((IPagedList)Model, page => Url.Action("Results", new { encryptedUrlViewModel = ViewBag.EncryptedUrlViewModel }))

The code may require some tweaks, untested in your scenario but it will be something like that, best of luck :)

You should really consider though, if you are wanting to carry the user's request in the URL across paging then one would consider why the form wasn't made as a GET request as opposed to a POST request in the first place. Any reason you particularly wanted it POST? I think GET will carry your Colors array properly, but make sure your view model is setup right. See this Haacked article for model binding to lists.

GONeale
  • 26,302
  • 21
  • 106
  • 149
  • Just to be clear. The most simple solution, as stated above, was to change the form method from POST to GET. – detroitpro Dec 22 '11 at 04:19
  • 1
    Haha.... doh. Oh my hand crafted coded solution to waste. Oh well :) You might need something like it one day.. =) – GONeale Dec 22 '11 at 13:37
  • coming into this a little late, but facing a similar challenge. So would you wrap the entire search form + results list in the Html form in order to get the correct model binding? Is the pagination part of the search viewmodel if the entire view is in the form, or is it a separate argument to the controller action? – Matthew Belk Nov 26 '12 at 18:51
  • Just to put this out there, you don't need to go through this whole Reflection scenario just to make this work. Your controller's ModelState property will already have all the correct property name/values in it. You can copy those to a RouteValueDictionary and attach that to any of the HtmlHelper link generating methods and get back the exact same values. – rossisdead Apr 11 '13 at 18:37
1

I had the same problem but with search parameters. We had a color parameter that was a list of color names the search engine was using. So you could tick black and blue and the result contained black as well as blue products.

I ended up using Unbound.

using Unbound;
Unbinder u = new Unbinder();

@Url.Action("Index", new RouteValueDictionary(u.Unbind(SearchParams)))

Which results in a link like:

/MyRoute?color[0]=black&color[1]=blue
Krisztián Balla
  • 19,223
  • 13
  • 68
  • 84