Search code examples
asp.net-mvcmodel-bindingasp.net-mvc-viewmodel

how to correctly use ViewModels


I created a simple application where I have a product list and search filter sidebar.(https://i.sstatic.net/pXwss.png) I had a problem with pagination(using PagedList) because after filtering results and moving on next page(https://i.sstatic.net/8qSnA.png) I was getting an empty page. Reason was clear. I was getting data in controller action parameters, so after clicking next page button, filter data was lost. So I decided to store this data somewhere.

I was told that I needed ViewModel, so I created another model FilterModel with side bar fields:

public class FilterModel {
    public string Manufacturer { get; set; }
    public string Name { get; set; }
    public int? MinPrice { get; set; }
    public int? MaxPrice { get; set; }
}

and this is Products model

public partial class Products {
    [Key]
    public int ProductID { get; set; }
    public string Manufacturer { get; set; }
    public string Name { get; set; }
    ......
    public int Price { get; set; }
}

And put them in ViewModel:

public class HomeViewModel {
    public IPagedList<Products> Repository { get; set; }
    public FilterModel FilterView { get; set; }
}

these are my controllers - HomeController that displays every product and SearchFilterController that should display filtered products:

public class HomeController : Controller {
    IProductRepository _repository;
    public HomeController(IProductRepository rep) {
        _repository = rep;
    }

    public ActionResult Index(int page=1) {
        ViewBag.Manufacturers = Manufacturers.ManufacturersList(_repository);
        var model =
            (from p in _repository.Product
             orderby p.Price descending
             select p).ToPagedList(page, 2);
        ViewBag.Title = "Home";
        return View(model);
    }
}

.

public class SearchFilterController : Controller {
        IProductRepository _repository;
        public SearchFilterController(IProductRepository rep) {
            _repository = rep;
        }

        [HttpPost]
        public ActionResult Filter(FilterModel filterModel, int page = 1) {
            ViewBag.Manufacturers = Manufacturers.ManufacturersList(_repository);
            var model =
                (from p in _repository.Product
                 where (p.Manufacturer == filterModel.Manufacturer || p.Name.Contains(filterModel.Name) || 
                    p.Price >= filterModel.MinPrice || p.Price <= filterModel.MaxPrice)
                 orderby p.Price descending
                 select p).ToPagedList(page, 1);
            ViewBag.Title = "Results";
            return View(model);
        }
    }

In both controllers I pass views PagedList<Products> model but they should be getting HomeViewModel I guess.

these are my views: Index(HomeController)

@model SmartPhoneCatalog.Models.HomeViewModel
@using System.Linq
@using PagedList
@using PagedList.Mvc
@using SmartPhoneCatalog.Domain.Abstract

...

<div id="root">
     @Html.Partial("_Sidebar", Model)

     @Html.Partial("_ProductsList", Model)
</div>

<br>
<div class="pagedList" data-sc-target="#products">
    @Html.PagedListPager(Model.Repository, page => Url.Action("Index", "Home", new { page }),
                PagedListRenderOptions.ClassicPlusFirstAndLast);
</div>

Filter(SearchFilterController)

@model SmartPhoneCatalog.Models.HomeViewModel
@using System.Linq
@using PagedList
@using PagedList.Mvc
@using SmartPhoneCatalog.Domain.Abstract

...

<div id="root">
    @Html.Partial("_Sidebar", Model)

    @Html.Partial("_ProductsList", Model)
</div>

<br>
<div class="pagedList" data-sc-target="#products">
    @Html.PagedListPager(Model.FilterModel, page => Url.Action("Filter", "SearchFilter", new { page }),
                PagedListRenderOptions.ClassicPlusFirstAndLast);
</div>

Partial View: _Sidebar

@model SmartPhoneCatalog.Models.HomeViewModel
@using System.Linq
@using PagedList
@using PagedList.Mvc

    <div id="filter" class="left">
        @using (Html.BeginForm("Filter", "SearchFilter")) {
            <div>
                <b>Manufacturer:</b> <br>
                <select name="manufacturer" class="form-control">
                    <option>@null</option>
                    @foreach (var item in ViewBag.Manufacturers) {
                        <option>@item</option>
                    }
                </select><br>

                <b>Name:</b> <br>@Html.EditorFor(model=>model.FilterModel.Name)<br>
                <b>Price From:</b> <br>@Html.EditorFor(model => model.FilterModel.Name)<br>
                <b>To:</b> <br>@Html.EditorFor(model => model.FilterModel.Name)<br>
                <button type="submit" value="search"><b>Search</b></button>
            </div>
        }
    </div>

I think I have everything done right in views. but I don't know how to pass them models correclty and object binding is done right or not.

Any suggestions?

Edit: OK, I did this in Controllers: Created HomeViewModel object and changed this

var model =
            (from p in _repository.Product
             orderby p.Price descending
             select p).ToPagedList(page, 2);
return View(model);

into this:

_homeView.Repository =
            (from p in _repository.Product
             orderby p.Price descending
             select p).ToPagedList(page, 2);
return View(_homeView);

now everything works except the pagination after filtering results(what was the reason why I did all of this :D). So I have to fix model binding


Solution

  • hi I have executed your code.. below are the changes

    Declare a Model like this

    public class FilterModel
    {
        public string Manufacturer { get; set; }
        public string Name { get; set; }
        public int? MinPrice { get; set; }
        public int? MaxPrice { get; set; }
    }
    
    public partial class Products
    {
        public int ProductID { get; set; }
        public string Manufacturer { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
    }
    
    public class HomeViewModel
    {
        public IPagedList<Products> Repository { get; set; }
        public FilterModel FilterView { get; set; }
    }
    

    Controller(s) action methods are

     public class PagingController : Controller
    {
        int pagesize = 2;
    
        public ActionResult Index(int page = 1)
        {
            HomeViewModel _HomeViewModel = new HomeViewModel();
    
            //call service and get products list
            AgentServiceReference.AgentServiceClient ServiceClient = new AgentServiceClient();
            var productlist = ServiceClient.GetProducts().ToPagedList(page, pagesize);
    
            //set IPagedList<Products> to our HomeViewModel Repository property
            _HomeViewModel.Repository = productlist;
    
            // set view bag  this we use for Drop Down list
            SetViewBags();
            ViewBag.Title = "Home";
            return View(_HomeViewModel);
        }
    
    
        [HttpGet]
        public ActionResult Filter(int page = 1)
        {
            //call service and get products list
            AgentServiceReference.AgentServiceClient ServiceClient = new AgentServiceClient();
            var productlist = ServiceClient.GetProducts().ToPagedList(page, pagesize);
    
            //set IPagedList<Products> to our HomeViewModel Repository property
            HomeViewModel _homeViewModel = new HomeViewModel();
            _homeViewModel.Repository = productlist;
            SetViewBags();
            return View("Index", _homeViewModel);
        }
    
        [HttpPost]
        public ActionResult Search(HomeViewModel HomeViewModel)
        {
            HomeViewModel _homeviewModel = new HomeViewModel();
            AgentServiceReference.AgentServiceClient ServiceClient = new AgentServiceClient();
    
            var productlist = ServiceClient.GetProducts();
            // do filter stuff here........
            var resultpagedList = productlist.ToPagedList(3, pagesize);
    
            _homeviewModel.Repository = resultpagedList;
    
            SetViewBags();
            return View("Index", _homeviewModel);
        }
    
        private void SetViewBags()
        {
            List<SelectListItem> manufactures = new List<SelectListItem>();
            for (int i = 0; i < 10; i++)
            {
                manufactures.Add(new SelectListItem { Text = "Text" + i, Value = "Value" + i, Selected = false });
    
            }
            ViewBag.Manufacturers = manufactures;
        }
    }
    

    Index.cshtml page

    @using System.Linq
    @using PagedList
    @using PagedList.Mvc
    @model EntityLayer.HomeViewModel
    @{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
    }
    @using (Html.BeginForm("Search", "Paging", FormMethod.Post))
    {
    <div id="root">
        @Html.Partial("_Sidebar", Model)
        @Html.Partial("_ProductsList", Model)
    </div>
    }
    

    _Sidebar.cshtml

    @model EntityLayer.HomeViewModel
    @using System.Linq
    @using PagedList
    @using PagedList.Mvc
    <div id="filter" class="left">
    <div>
        <b>Manufacturer:</b>
        <br>        
        @Html.DropDownListFor(model => model.FilterView.Manufacturer, ViewBag.Manufacturers as IEnumerable<SelectListItem>)
        <b>Name:</b>
        <br>@Html.TextBoxFor(model => model.FilterView.Name)<br>
        <b>Price From:</b>
        <br>@Html.TextBoxFor(model => model.FilterView.MaxPrice)<br>
        <b>To:</b>
        <br>@Html.TextBoxFor(model => model.FilterView.MinPrice)<br>
        <button type="submit" value="search"><b>Search</b></button>
    </div>
    

    and finally _ProductsList.cshtml

    @model EntityLayer.HomeViewModel
    @using System.Linq
    @using PagedList
    @using PagedList.Mvc
    <table>
    <tr>
        <th>Name</th>
        <th>Price</th>
    </tr>
    
    @foreach (var item in Model.Repository)
    {
        <tr>
            <td>@item.Name</td>
            <td>@item.Price</td>
        </tr>
    }
    

    <div class="pagedList" data-sc-target="#products">
    @Html.PagedListPager(Model.Repository, page => Url.Action("Filter", "Paging", new { page }),
                PagedListRenderOptions.ClassicPlusFirstAndLast);
    

    • Change the name spaces of EntityLayer.HomeViewModel accordingly
    • The Approach you followed may not be correct since we are passing entire HomeViewModel every time where a partial view indeed needs specific model.
    • it would be better if you achecive the same using ajax request Please note I didn't apply layout as you have provided images. Result images are On Load First Paging Given Model Values Model Binding When Post

    Hope it helps you