Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.net Mvc3 webgrid and paging

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>();
    }
}
like image 462
adt Avatar asked Jun 14 '11 13:06

adt


3 Answers

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;
    });
});
like image 119
Darin Dimitrov Avatar answered Nov 04 '22 00:11

Darin Dimitrov


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).

like image 40
escape-llc Avatar answered Nov 04 '22 00:11

escape-llc


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!

like image 23
Varun Chatterji Avatar answered Nov 04 '22 02:11

Varun Chatterji