Asp.net Mvc3 webgrid and paging
Asked Answered
L

4

10

I am trying to learn Asp.net mvc. I know its different from forms and i need to change my way of thinking probably. My problem is about webgrid . When i add webgrid to my page and hit search button with Post it renders table with pager and so on. But links on the pager is not posting form they are just links and i lost all my form's data.

Controller has two index methods one is for get and other is for post. For get i do nothing, I just create new viewmodel in this case Search class and set it to view. For my post method i grab my view model do search and set filled viewmodel to view.

problem : webgrid renders pager as links so it will enter to the Index for get but since it is not a post request i dont have any form fields filled and my search will not provide the very same result set.

Maybe example code can explain it better.

View:

<form action="" method="post">

Esas no : @Html.TextBoxFor(x=>x.Name)
Yil : @Html.TextBoxFor(x=>x.Year)

<input type="submit" value="Search" />

<hr />
@ViewBag.Message
<hr />

@{ var grid = new WebGrid(Model.Results,rowsPerPage:5);}

@grid.GetHtml(tableStyle:"table",htmlAttributes:new {id="tbl"} )

</form>

Here is My Controller: Search occures in Index Post method and it has just my viewmodel class.

    private ISearchContext _sc;

    public  MyController(ISearchContext sc)
    {
        _dc = dc;
    }

    //
    // GET: /Dava/

    public ActionResult Index()
    {
        var search = new Search();
        ViewBag.Message = "";
        return View(search);
    }

    [HttpPost]
    public ActionResult Index(Search search)
    {

        Search sres = _dc.SearchFromRepository(search);
        ViewBag.Message = String.Format("Count:{0} ",sres.Results.Count);
        return View(sres);
    }

Search model Class is like:

public class Search
{
    public int Year { get; set; }
    public string Name { get; set; }


    public IList<Item> Results { get; set; }

    public Search()
    {
        Results = new List<Item>();
    }
}
Lum answered 14/6, 2011 at 13:8 Comment(0)
L
9

One way to solve this issue is to use javascript and subscribe for the click event of any of the pager links and then fetch the value of the desired page, inject it into a hidden field on the form and submit the form to the server so that the other two values are also sent.

So start by adding a Page nullable integer property on your Search view model and a corresponding hidden field into the form which will contain the selected page number:

@Html.HiddenFor(x => x.Page, new { id = "page" })

Then all you need is a small javascript snippet into the page to subscribe for the .click event of the pager links:

$(function () {
    $('tfoot a').click(function () {
        // when the user clicks on any of the pager links
        // try to extract the page number from the link and
        // set the value of the hidden field
        var page = this.href.match(/page=([0-9])+/)[1];
        $('#page').val(page);

        // submit the form so that the POST action is invoked
        // passing along the search criteria (Name and Year) along
        // with the page hidden field value to the Index action
        $('form').submit();

        // cancel the default action of the link which is to simply redirect
        // to the Index action using a GET verb.
        return false;
    });
});
Lutestring answered 14/6, 2011 at 13:30 Comment(5)
your suggestion works well, but am i the only one who thinks such a trivial task shouldn't be that hard to accomplish. I need to add some code also for sorting and so on...Lum
@adt, well, it's a perfectly normal reaction if you are a long time classic WebForms developer. Classic WebForms are leaky abstractions and hide how the web actually works (things like the HTTP protocol, HTML forms, javascript have been hidden behind some events, PostBacks, ViewStates, etc...). That's why you probably find it strange but it is perfectly normal :-)Lutestring
@adt, of course you're right, it is quite strange that this common scenario is not easy. However, WebGrid is just one possible solution for rendering grids with its pros and cons. Apparently it does not support filtering (neither with some integrated solution, nor with posting back form data easily). Maybe it is not the right choice for your concrete case? You can find other solutions with various level of abstractions and various level of support for the "common" features like sorting, paging, filtering, grouping, hierarchy, etc. Unfortunately most of them are not for free...Scleritis
thank your for your comments i knew i had to change my way of thinking, but still i am thinking that way : " well of course i want full power over html ( no ugly viewstate and no crappy update panel) and want to use DI on my controllers etc, but i really dont want to spend a lot of time grid pager and sorting and whole team have to deal with lots of things which will slow down development process".Lum
Please see my solution using partial view. Using my approach you don't need ViewState, TempData, ViewBag, Javascript or any of those complex approaches.Chivy
H
5

Here is a workaround that doesn't use JavaScript.

The problem as I see it, the paging links do not receive any route information that must be preserved, like a search filter. IMO this is a blatant oversight! A little extra thought here would have saved lots of headache!

This technique "throws away" the WebGrid's built-in paging, and uses a Helper to generate the paging links, along with the precious route data we want.

Once completed, you simply render the WebGrid as the grid only, and use the Helper to make the paging links. One advantage here is you can put those at the top and bottom, which we like to do.

I attempted to use similar CSS to what is provided in the Pager.css that NuGet puts into your solution. The helper should be complete enough for some of you, but it is easily extended.

New New New I just updated the helper with an Ajax version. I am a little n00b with Razor helpers, so i couldn't figure out how to re-factor it to use a common template; anyone please? The important extra detail there is to pass in the AjaxOptions and make sure to use POST as the verb, otherwise you may not end up in the correct controller method.

Helper (App_Code/LocalHelpers.cshtml):

@helper DoPager(System.Web.Mvc.HtmlHelper hh, string pageActionName, WebGrid grid, int maxPageLinks, object rvd) {
<div class="pager">
<div class="pageof">Page <b>@(grid.PageIndex + 1)</b> of <b>@grid.PageCount</b></div>
@if (grid.PageCount > 1) {
<ul>
<li>
@{ RouteValueDictionary rvdp1 = new RouteValueDictionary(rvd);
   rvdp1.Add("Page", 1);
}
@hh.ActionLink("<<", pageActionName, rvdp1)
</li>
@{ int start = Math.Max(0, grid.PageIndex - maxPageLinks / 2); }
@for (int ix = 0; ix + start < grid.PageCount; ix++) {
    int pageno = start + ix + 1;
    var css = hh.Raw(pageno - 1 == grid.PageIndex ? " class=\"highlighted\"" : "");
    RouteValueDictionary rvdp = new RouteValueDictionary(rvd);
    rvdp.Add("Page", pageno);
<li@css>
@hh.ActionLink(pageno.ToString(), pageActionName, rvdp)
</li>
    if (ix >= maxPageLinks) { break; }
}
<li>
@{ RouteValueDictionary rvdpX = new RouteValueDictionary(rvd);
   rvdpX.Add("Page", grid.PageCount);
}
@hh.ActionLink(">>", pageActionName, rvdpX)
</li>
</ul>
}
</div>
}
@helper DoAjaxPager(System.Web.Mvc.AjaxHelper aa, System.Web.Mvc.Ajax.AjaxOptions aopts, System.Web.Mvc.HtmlHelper hh, string pageActionName, WebGrid grid, int maxPageLinks, object rvd) {
<div class="pager">
<div class="pageof">Page <b>@(grid.PageIndex + 1)</b> of <b>@grid.PageCount</b></div>
@if (grid.PageCount > 1) {
<ul>
<li>
@{ RouteValueDictionary rvdp1 = new RouteValueDictionary(rvd);
   rvdp1.Add("Page", 1);
}
@aa.ActionLink("<<", pageActionName, rvdp1, aopts)
</li>
@{ int start = Math.Max(0, grid.PageIndex - maxPageLinks / 2); }
@for (int ix = 0; ix + start < grid.PageCount; ix++) {
    int pageno = start + ix + 1;
    var css = hh.Raw(pageno - 1 == grid.PageIndex ? " class=\"highlighted\"" : "");
    RouteValueDictionary rvdp = new RouteValueDictionary(rvd);
    rvdp.Add("Page", pageno);
<li@css>
@aa.ActionLink(pageno.ToString(), pageActionName, rvdp, aopts)
</li>
    if (ix >= maxPageLinks) { break; }
}
<li>
@{ RouteValueDictionary rvdpX = new RouteValueDictionary(rvd);
   rvdpX.Add("Page", grid.PageCount);
}
@aa.ActionLink(">>", pageActionName, rvdpX, aopts)
</li>
</ul>
}
</div>
}

View:

<center>
@LocalHelpers.DoPager(Html, "Index", grid, 10, new { CurrentFilter = ViewBag.CurrentFilter })
</center>
@grid.Table(
    tableStyle: "centerit",
    columns: grid.Columns(
        grid.Column(format: @<span>@Html.ActionLink("Edit", "Edit", new { id = item.ID }) | @Html.ActionLink("Details", "Details", new { id = item.ID }) | @Html.ActionLink("Delete", "Delete", new { id = item.ID })</span>),
            grid.Column("PartNumber", "Part Number"),
            grid.Column("Description", "Description"),
            grid.Column("Regex", "Regex")
            )
        )
<center>
@LocalHelpers.DoPager(Html, "Index", grid, 10, new { CurrentFilter = ViewBag.CurrentFilter })
</center>

In my view, I am recycling the "CurrentFilter" to know what to filter on. This connects to the Controller Action (not pictured).

Hands answered 12/8, 2011 at 15:37 Comment(4)
What about sorting? Is route information preserved for the sorting links?Tellurite
It is set up to pass through any values you choose to "regenerate" like sorting parameters, filter parameters, etc. but you must put them in where you are adding the grid. If you look at the last parameter, that is the place to put stuff you want to "come back" to your action.Hands
thanks for the flame old boy i don't even do it this way i was just trying to address the question, that's what this site is about! It's not minus so some people found it useful, again the point of this site.Hands
more abut this solution see (Helpers) = > a2z.googlecode.com/svn/trunk/PresentationMVC/App_Code/… And (View) => a2z.googlecode.com/svn/trunk/PresentationMVC/Views/Job/… Thank @HandsMariellemariellen
C
3

Ok. I have a more elegant solution using AJAX and Partial Views that should resolve this issue once and for all

This is my model:

public class SearchResultModel
{
        public string SearchText{ get; set; }
        public List<YourObject> Results { get; set; }
        public int TotalResults { get; set; }
}

The search view is structured like this:

@model SearchResultModel
@using (Ajax.BeginForm("SearchAction", "SearchController", new AjaxOptions{UpdateTargetId = "data-grid", HttpMethod="Post"}))
{
        @Html.TextBoxFor(m => m.SearchText)
        <input class="myButton" type="submit" value="Search" />
}
<br />
<div id="data-grid">
       @Html.Partial("SearchResults", new SearchResultModel())
</div>

The SearchResults partial view is:

@model SearchResultModel
@{
    if (Model.Results != null && Model.Results.Count > 0)
    {
            var grid = new WebGrid(canPage: true, rowsPerPage: 10, canSort: true, ajaxUpdateContainerId: "grid");
            grid.Bind(Model.Results, rowCount: Model.TotalResults, autoSortAndPage: false);
            grid.Pager(WebGridPagerModes.All);

            @grid.GetHtml(htmlAttributes: new { id = "grid" },
            columns: grid.Columns(
                grid.Column("YourColumn1"),
                grid.Column("YourColumn2"),
                grid.Column("YourColumn3")
            ),
            tableStyle: "datatable",
                rowStyle: "datatable-normal",
                    alternatingRowStyle: "datatable-alt"
            );
    }
    else
    {
    <span>No Results</span>
    }
}

Finally, the Controller is:

public class SearchController
{
        public ActionResult SearchAction(SearchResultModel model)
        {
            return RedirectToAction("SearchResults", new { id = model.SearchText });
        }

        public ActionResult SearchResults(string id)
        {
            string searchText = id;
            int page = 1;
            if(Request["page"] != null)
                int.TryParse(Request["page"], out page);

            SearchResultModel model = new SearchResultModel();
            //Populate model according to search text and page number
            //........
            //........
            return PartialView(model);
        }
}

Hope this will help save someone some time and angst!

Chivy answered 11/6, 2012 at 19:49 Comment(0)
H
0

My answer envolves keeping your search on Session, and nothing more than that. The solution is good because you can adapt it to your real world situation, and doesnt need specific classes or JQuery.

The magic trick happens inside your Index ActionResult (or your default ActionResult, that will render the grid page at its default behavor).

Code example:

    [HttpGet]
    public ActionResult Index()//My default action result that will render the grid at its default situation
    {
        SearchViewModel model = new SearchViewModel(); 

        if (Request.IsAjaxRequest()) //First trick is here, this verification will tell you that someone sorted or paged the grid.
        {
            if (Session["SearchViewModel"] != null) //If session is not empty, you will get the last filtred values from it.
                model = (SearchViewModel)Session["SearchViewModel"];
        }
        else // If it is not an AjaxRequest, you have to clear your Session, so new requests to Index with default behavior won't display filtred values.
        {
            Session["SearchViewModel"] = null;
        }

        model.GridResult = ExecuteFilter(model); // OPITIONAL! This code dependes on how is your real world situation. Just remember that you need to return a default behavior grid if the request was not called by the WebGrid, or return filtred results if WebGrid requested.
        return View(model);
    }

So, this will be your default ActionResult. It will verify if the request was called by the WebGrid paging or sorting event, to decide if returns filtred results or normal behavior result.

Next step is the search POST ActionResult:

    [HttpPost]
    public ActionResult Index(SearchViewModel pesquisa) // IMPORTANT!! It is necessary to be the SAME NAME of your GET ActionResult. The reason for that I know, but won't discuss here because it goes out of the question.
    {
        SearchViewModel model = new SearchViewModel();
        model.GridResult = ExecuteFilter(pesquisa); // Execute your filter
        Session["SearchViewModel"] = model; //Save your filter parameters on Session.
        return View("Index", model);
    }

Thats it. The Index.cshtml doesn't have any trick. Just a SearchForm to the ActionResult Index, passing my SearchViewModel as parameter.

Why this solution works?

Well, when you click to sort or page, the WebGrid execute a JavaScript similar to this:

$('#yourGrid').load('it pass the url used to display your current Page, and some paging or sorting parameters, but those are used by the WebGrid')

Since it does a .load() method, the request will be a GET, and will hit your Index GET ActionResult. But it is an AJAX call, so our magic trick will execute the filter again with the parameters you saved on Session.

The unique detail I alert, is about your default grid behavior. The GET Index ActionResult MUST EVER RETURNS a valid grid result, not matter if it has or not filters on Session.

Homburg answered 20/3, 2013 at 15:18 Comment(1)
You can change Session to Cache if its fit better.Homburg

© 2022 - 2024 — McMap. All rights reserved.