MVC page that displays multiple, related, ViewModels updating with ajax - c#

I have some simple list management pages, each is strongly typed against the following ViewModel
public class ListManagementIndexViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
}
I have Views for Category, Priority, Status and a few other drop downs, there's some AutoMapper configuration that ensures the Name property is bound to whatever the Domain model might have for the entity. The idea is that the systems administrators are able to manage what's in these lists (Create new, Edit existing and Delete). This gives me an Index view for each like this:
I would like to put all of these on a single page, rather than have separate views for each. I think partials are probably the way to do this, but I'm not clear whether that's a recommended approach or not?
Also I'm not clear on how to handle the Controller(s). As things stand; with the PriorityController I have the following Index ActionResult
public ActionResult Index(string searchString, string currentFilter, int? page)
{
if (Request.HttpMethod == "GET")
searchString = currentFilter;
ViewBag.CurrentFilter = searchString;
int pageNumber = page ?? 1;
var query = Session.QueryOver<Priority>();
if (!string.IsNullOrWhiteSpace(searchString))
query.WhereRestrictionOn(p => p.PriorityName)
.IsInsensitiveLike(String.Format("%{0}%", searchString));
var result = query.OrderBy(p => p.PriorityName).Asc.List();
var viewModel = AutoMapper.Mapper.Map<IEnumerable<Priority>,
IEnumerable<ListManagementIndexViewModel>>(result)
.ToPagedList(pageNumber, PageSize);
return View(viewModel);
}
While I could move that to a 'Master' single list management page I would also need to do the same for the other list types, but then what would my ViewModel need to look like, there are currently 5 lists to manage, so I have 5 controllers doing exactly the same thing, only the types vary. If I were to put all that on 1 'Master' single view page I would somehow need to stuff all 5 PagedList collections into 1 ViewModel and then in the 'Master' single version pass the data off to the relevant Partials razor code?
Also how would I handle the other Controller actions (Edit, Create and Delete)?
Update
Thinking on my feet - if the unified / single view page ViewModel were:
public class ListManagementIndexViewModel
{
public Dictionary<string, IPagedList<T>> ManagementLists { get; set; }
}
and the SinglePageController were
public ActionResult Index(string searchString, string currentFilter, int? page)
{
var priorities = GetPriorities(searchString, currentFilter, page);
var categories = GetCategories(searchString, currentFilter, page);
Dictionary<string, IPagedList> viewModel
= new Dictionary<string, IPagedList<T>>();
viewModel.Add("Priorities", priorities);
viewModel.Add("Categories", categories);
return View(viewModel);
}
I could move the existing logic out to a private method
private IPagedList<Priority> GetPriorities(string searchString,
string currentFilter, int? page)
{
if (Request.HttpMethod == "GET")
searchString = currentFilter;
ViewBag.CurrentFilter = searchString;
int pageNumber = page ?? 1;
var query = Session.QueryOver<Priority>();
if (!string.IsNullOrWhiteSpace(searchString))
query.WhereRestrictionOn(p => p.PriorityName)
.IsInsensitiveLike(String.Format("%{0}%", searchString));
var result = query.OrderBy(p => p.PriorityName).Asc.List();
var priorities = AutoMapper.Mapper.Map<IEnumerable<Priority>,
IEnumerable<ListManagementIndexViewModel>>(result)
.ToPagedList(pageNumber, PageSize);
return priorities;
}
Then in my View the model would contain the ManagementLists collections and I could foreach the results into html tables as I'm currently doing but on separate pages.
However a paging through the results would then result in all lists tables paging rather than just the Priorities table. How would I make each update with Ajax, so paging in one table would only update the list in that one table? If I were to re-include searching, which is currently in the ActionResult method, but not implemented in the UI that would also need to update just the Priorities table. For example if I entered "High" in the search for Priority I would only want to query the Priority table and update it and not run 5 queries to the database to search for "High" in Category or any of the other lists.

I think you should only use a partial if you intend to use it again. I see too often people creating them to help break down the code, but they're never used again.
It would be more organised to have the 5 lists under the one page, that way they're all in the one model and are centrally managed by one controller/model.

Related

How to implement pagedlist in mvc when using view models?

I am using viewmodel concept in mvc 4. I am posting data to db and as soon as post displaying in grid below also. So I have used view model concept. This is my viemodel class
public class MyViewModel
{
public document upload_document { get; set; }
public IList<document> Upload_List { get; set; }
}
I have many fields in document so I am not posting fields. I have gone through some articles and they are returning something like this: return View(students.ToList());where student is var. But I am returning model in all action methods. Also in view they have written
#model PagedList.IPagedList<ContosoUniversity.Models.Student>
#using PagedList.Mvc;
but I have this in my view
#model c3card.Dal.EDModel.MyViewModel
So, how can return model that contains pagenum,page size? How can I use this pagedlist concept when using view models?
If you need to map your entities to view models and still be able to page through them, then you need to use StaticPagedList. In order for the standard method of using PagedList to work, it needs to be passed a queryable (unevaluated), so that it can limit the query to only pull the proper number of records. If you're mapping to a view model, though, then that's going to evaluate the query.
What you need is something like:
public ActionResult Students(int? page)
{
var pageNumber = page ?? 1;
var pageSize = 10;
var totalStudents = db.Students.Count();
var students = db.Students.Skip((pageNumber - 1) * pageSize).Take(pageSize);
var model = // map `students` to your view model
var pagedList = new StaticPagedList<MyViewModel>(model, pageNumber, pageSize, totalStudents);
return View(pagedList);
}
Then, the model for your view is:
#model PagedList.IPagedList<Namespace.To.MyViewModel>

Post IEnumerable collections from view in mvc

I am developing an ASP.NET MVC application, populating dropdownlist to view working fine in get method. I am populating to view like this in my controller
[HttpGet]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, int i = 0)
{
employeeSuperClass.FamilyDetailsFields = new FamilyList();
employeeSuperClass.FamilyDetailsFields.familyMembersList.Insert(0, new EmployeeFamilyTable());
*employeeSuperClass.FamilyDetailsFields.employee_RelationTable = dt.GetRelations();*
*employeeSuperClass.FamilyDetailsFields.employee_BloodGroupTable = dt.GetBloodGroups();*
*employeeSuperClass.FamilyDetailsFields.employee_NationalityTable = dt.GetNationalities();*
return View("CreateEmployeeFamilyDetails", employeeSuperClass);
}
Please look into starred lines
In case if there is any errors in model am getting null reference
In post action method look like this
[HttpPost]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, string Command)
{
if (ModelState.IsValid)
{
return("some view");
}
else
{
return view(employeeSuperClass);
}
}
I know again we have to create instance to populate dropdownlist this is rubbish to do same again and again
Can anyone explain how to store dropdown list collection in view separately and post them also with model?
(note: employee_relationTable is IEnumerable collection and it is a relationtable type this table contains relation id and relationname fields and am using this table in this class like below
public IEnumerable<EmployeeRelationTable> employee_RelationTable { get; set; }
For rest also am using same approach
Can we post employee_RelationTable from view and how?
Please help and your help would be greatly appreciated
Assuming dt is a member field available to all methods, what you can do is DRY up the population of the DropDowns in the View Model in a separate method, e.g.
private void PopulateDropDownsOnViewModel(EmployeeSuperClass model)
{
model.FamilyDetailsFields = new FamilyList
{
employee_RelationTable = dt.GetRelations(),
employee_BloodGroupTable = dt.GetBloodGroups(),
employee_NationalityTable = dt.GetNationalities()
}
}
Which can be used in the Get:
[HttpGet]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, int i = 0)
{
PopulateDropDownsOnViewModel(employeeSuperClass);
return View("CreateEmployeeFamilyDetails", employeeSuperClass);
}
and in the Post (and any other controller actions which need the drop downs)
[HttpPost]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, string Command)
{
if (ModelState.IsValid)
{
return("some view");
}
else
{
PopulateDropDownsOnViewModel(employeeSuperClass);
return View(employeeSuperClass);
}
}
If the drop downs are static, you can also look at caching these to prevent wasted IO to the database.
But no, don't serialize the the data in the View somehow (remember WebForms ViewState?) or fetch data from the View directly - this violates the MVC paradigm - the controller is responsible for providing data for the View to render.

Persisting Dropdown information when ModelState is not valid

I'm having some issues with my DropDownLists, because when I post the information and my Model is not valid it comes back "empty" to the page triggering an error exactly like this question.
I've used the solution proposed there and it fixed my problem. Anyway, I wanted to avoid querying the database every time my ModelState is not valid and I came with this approach. I would like to know if it is valid or if there are better ways to do it now, considering that instead of MVC2 (which was the MVC version from the question) I'm now using MVC 5, maybe they added something new to tackle this.
What I've done was to use the TempData to persist the information when my model is not valid.
public class ViewModel
{
[DisplayName("Project")]
public int ProjectID { get; set; }
public List<SelectListItem> Projects { get; set; }
//Other fields
}
Now my Create() Action (that populates Projects)
[HttpGet]
public ActionResult Create()
{
ViewModel vmodel = new ViewModel();
vmodel.Projects = db.GetProjects(User.Identity.Name).Select(x => new SelectListItem { Text = x.Description, Value = x.Id }).ToList();
TempData["Projects"] = vmodel.Projects;
return View(vmodel);
}
And my post would be like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ViewModel vmodel)
{
//Clear TempData (in theory will clear my tempdata when read, so if this controller redirects to another action my tempdata will be clear)
List<SelectListItem> projects = (TempData["Projects"] as List<SelectListItem>);
if (ModelState.IsValid)
{
//...
}
//If it got here it's going back to the screen.
//Repopulate the TempData (allowing it to exist one more trip)
TempData["Projects"] = projects;
vmodel.Projects = projects
return View(atendimento);
}
Is this approach a good one? Is there a better way to achieve that without querying the database every single time?
Thanks a lot!
You don't need to use TempData at all as you have a property in your view model to hold the dropdown items.
public ActionResult Create()
{
ViewModel vmodel = new ViewModel();
vmodel.Projects = GetProjects();
return View(vmodel);
}
private List<SelectListItem> GetProjects()
{
return db.GetProjects(User.Identity.Name)
.Select(x => new SelectListItem { Text = x.Description,
Value = x.Id }).ToList();
}
And in the view
#Html.DropDownListFor(s=>s.ProjectID,Model.Projects)
And in your HttpPost action, If ModelState is not valid, Reload the Projects collection again (because http is stateless)
if(ModelState.IsValid)
{
// to do :Save and redirect
}
model.Projects = GetProjects();
return View(model);
You may cache the Projects so that you do not need to hit the database every time, if you are too much worried about performance.
Personally, I wouldn't worry about querying the database each time for this kind of operation.
What if projects are added/deleted? This could be the reason the save failed (selected project deleted) and the user would never realise it.
I usually write a method to populate all of my view model's SelectListItems and then use this in my Get and in my Post if the validation fails.

Proper way to POST data MVC

Warning: This is my first web app.
I have 4 models, views and controllers. Lets call them A, B, C, D(ex. ModelA, ControllerA, ViewA). They are all basic views with list scaffolding.
/ControllerA/Index
User starts at ViewA and Selects an the first item, which redirects the user to ViewB
/ControllerB/Function?Aid=1
ViewB shows another list based on Selection from ViewA. Then the user Selects again is is redirected to ViewC
/ControllerC/Function?Aid=1&Bid=2
ViewC shows another list based on Selections from ViewA and ViewB. Then the user Selects again is is redirected to ViewD.
/ControllerD/Function?Aid=1&Bid=2&Cid=3
ViewD shows another list based on Selections from ViewA, ViewB, and ViewC, Then the user Selects again.
At this point I would like to POST Aid, Bid, Cid, and Did and save them in my database. Ideally the user would click the link, the data would be posted and then the site would redirect the user back to the homepage. Should I create another model and controller to Handle the post? I thought about trying to do the POST from controllerD but that doesn't seem like the proper way to do this.
The msdn tutorials only show posting directly from a view with a strongly typed model. I kinda stuck and I would prefer not to make this a complete mess.
Edit for Code
Controller
public ActionResult myFunction(int Aid = 0, int Bid, int Cid)
{
//query D stuff here
if (D == null)
{
return HttpNotFound();
}
return View(D.ToList());
}
[HttpPost]
[InitializeSimpleMembership]
public ActionResult CreateQuote(int Aid, int Bid, int Cid, int Did)
{
Quote myQuote = new Quote();
myQuote.Customer_ID_FK = (int)Membership.GetUser().ProviderUserKey;
myQuote.A_ID_FK = Aid;
myQuote.B_ID_FK = Bid;
myQuote.C_ID_FK = Cid;
myQuote.D_ID_FK = Did;
if (ModelState.IsValid)
{
db.Quotes.Add(myQuote);
db.SaveChanges();
db.Quotes.Max();
int mymax = db.Quotes.Max(q => q.ID);
return RedirectToAction();
}
return View(D.ToList());
}
[HttpPost]
[InitializeSimpleMembership]
public ActionResult CreateQuote(Quote myQuote)
{
myQuote.Customer_ID_FK = (int)Membership.GetUser().ProviderUserKey;
if (ModelState.IsValid)
{
db.Quotes.Max();
int mymax = db.Quotes.Max(q => q.ID);
db.Quotes.Add(myQuote);
db.SaveChanges();
return RedirectToAction();
}
return View(D.ToList());
}
It usually makes sense to put your post handler in the controller it's related to. This isn't always the case, as sometimes it would make more sense to make a new controller to handle all posts related to a certain task. You should also understand the distinction between a method IN a controller, and a controller. A controller is just a class that inherits from System.Web.Mvc.Controller and can have methods just like any other class. A perfectly reasonable controller could look like this:
public class DController : Controller
{
//GET /d
public ActionResult Index()
{
//MyModel is a class that would contain the logic to display
//the selections from A, B, and C
var model = new MyModel();
return View(model);
}
//POST /d/saveresults
//We only want this method to accept POST
[HttpPost]
public ActionResult SaveResults(MyEntity data)
{
var model = new MyModel();
model.SaveResultsToDatabase(data);
return Redirect("/");
}
}
The important thing in a controller is to keep logical processing to a minimum. There's nothing wrong with having an if statement here and there, but the majority of your logic should be handled by your model. A controller is there primarily to pass data between your views and models.

Call static function in view mvc

i'm working on a mvc project and i want to display a sponsorimage on each page.
But i'm having difficulties with showing them into the shared layout page that is rendered on with each view.
I've created a function in my Domain service class where i search the student's school, because the school is linked to a country, not the student.
When i got that countryId, i search through the countries of each Advert where the countryId is equal to the school's countryId. When that's the case, i look for the sponsor of that particular advert, put them into a SponsorList, select a random sponsor from that SponsorList and return the SponsorCompany (because i renamed each sponsorimage to the companyname).
Now i want to call that function into the shared layout, so every time the pages renders, a random sponsorimage is showed for that particular student. But i dont know how to call that function because shared layout has no controller class.
public String advertsForCountry()
{
String studentSchool = finder.getLoggedStudent().SchoolId;
int studentCountry = db.Schools.Find(studentSchool).CountryId;
List<Sponsor> sponsorsForStudent = new List<Sponsor>();
List<Advert> adverts = db.Adverts.ToList();
foreach(Advert adv in adverts)
{
foreach(Country cntry in adv.Countries)
{
if(cntry.CountryId == studentCountry)
{
sponsorsForStudent.Add(adv.Sponsor);
}
}
}
Random random = new Random();
int randomSP = random.Next(0, sponsorsForStudent.Count()-1);
string sponsorAdvert = sponsorsForStudent.ElementAt(randomSP).SponsorCompany;
return sponsorAdvert;
}
Sorry English is not my native language.
To expand upon #SLaks suggestion;
Create an action that is marked with a ChildActionOnlyAttribute (this prevents it from being called via a regular HTTP request). Here's an example from my website:
[HttpGet]
[ChildActionOnly]
public ActionResult RandomQuote()
{
var model = _services.GetRandomQuote();
return PartialView("_QuoteOfTheMomentWidget", model);
}
This child action gets called in the _Layout via a simple #Html.Action("randomquote").
Create a controller action that returns a partial view.
public PartialViewResult SponsoredAdvert()
{
var model = new SponsoredAdverModel();
model.AdvertText = _domainService.advertsForCountry();
return PartialView("myView", model);
}
Place the method in a suitable controller (HomeController would make sense given that this is for your Layout.cshtml) and use RenderAction in your view:
#Html.RenderAction("MyAction", "MyController")
As you can see, RenderAction allows you to specify the controller, which means that you can use this within your Layout.cshtml even though it, in itself, is not associated with a specific controller.

Categories