I have run into a caveat with regards to my approach to ASP.NET MVC and viewmodels. Essentially what I do is build a viewmodel in controller/action which merges models together and then passes it to the view.
[HttpGet]
public ActionResult MyAction1()
{
List<StaffModel> staffList = new List<StaffModel>();
var qryStaff = context.Staff.Select(c => new { c.ID, c.name});
foreach (var item in qryStaff )
{
StaffModel myStaffViewModel = new StaffModel
{
ID = item.ID, Name = item.Name
};
staffList.Add(myStaffViewModel );
}
So I do the above process and also do it with employees, exactly the same and then put it into employeeList. I then create my viewModel as the view.
EmployeeStaffViewModel viewModel = new EmployeeStaffViewModel
{
Staff = staffList,
Employee = employeeList
};
I then return the view. I have used employee & staff as an example. I actually have more models I add to the viewModel EmployeeStaffViewModel . It's getting quite big all within the controller action. Should I be creating a ViewModel as a class and then instantiating it in my controller so all the linq and foreach goes in the Model. Therefore I can use it in another controller action.
Thank you for any advice. Will be greatly received.
Lets say you have 3 classes in your ~/Models folder
StaffModel.cs
public class StaffModel
{
public int ID { get; set; }
public string Name { get; set; }
public static Func<Staff, StaffModel> Project = item => new StaffModel
{
ID = item.ID,
Name = item.Name
};
}
EmployeeModel.cs
public class EmployeeModel
{
public int ID { get; set; }
public string Name { get; set; }
public static Func<Employee, EmployeeModel> Project = item => new EmployeeModel
{
ID = item.ID,
Name = item.Name
};
}
EmployeeStaffViewModel.cs
public class EmployeeStaffViewModel
{
public EmployeeStaffViewModel()
{
Staff = new List<StaffModel>();
Employee = new List<EmployeeModel>();
}
public List<StaffModel> Staff { get; set; }
public List<EmployeeModel> Employee { get; set; }
}
The StaffModel and EmployeeModel both have a static Func<> that will map your db entity to your models. These Funcs can be used in your linq queries and expressions which you'll see below.
Your controller action is where you will retrieve your entities from your context. You can simplify your code to not have as many lines as you do.
MyController.cs
[HttpGet]
public ActionResult MyAction1()
{
var model = new EmployeeStaffViewModel();
model.Staff = context.Staff.Select(StaffModel.Project); //Select Staff to StaffModel List
model.Employee = context.Employee.Select(EmployeeModel.Project); //Select Employee to EmployeeModel List
return View(model);
}
Related
I have view model class
public class IndexViewModel
{
public int ListOneId { get; set; }
public List<SelectListItem> ListOne { get; set; }
public int ListTwoId {get; set; }
public List<SelectListItem> ListTwo { get; set; }
public int ListThreeId {get; set; }
public List<SelectListItem> ListThree { get; set; }
public int ListFourId {get; set; }
public List<SelectListItem> ListFour { get; set; }
// ...
}
I can populate the view model in the controller class
[HttpGet]
public IActionResult Index()
{
var model = new IndexViewModel();
model.ListOne = _context.ListOne
.Select(x => new SelectListItem() { Text = x.Text, Value = x.Value })
.ToList()
// ...
return View(model);
}
But because the view model populating appears in multiple places, I would like to encapsulate it into its own class.
The question is, what is the suitable name for such a class?
It's not a builder, it's not a factory. Is it seeder, populator or helper? What common suffix can be used here?
Or should it be part of the ViewModel itself (for example with a method that has DbContext as an parameter)?
Update
The question is not about how to name the SelectList Provider but instead where to populate the view model (which class to call this SelectList provider from, whether it's repository, factory, DbContext or else) to minimize duplicates.
To be honest if you add a factory method then it easily becomes a factory pattern. That's what I would do personally
public static List<SelectListItem> GetTheList(int id) =>
id switch
{
1 => ListOne,
2 => ListTwo
//...
_ => throw new Exception("Please select a valid list")
};
//use it like:
List<SelectListItem> list = IndexSelectListFactory.GetTheList(1); //gets ListOne
Notice I have named it IndexSelectListFactory here, just add the suffix that matches the pattern you end up choosing, in this case "-Factory"
But if you do not want to do this, then just keep it simple like "IndexSelectLists"
Let's say I have complex view model with a lot of data such as lists of countries, products, categories etc. for which I need to fetch from the database every time I create the ViewModel.
The main problem I want to fix is that when I handle POST actions and some TestModel was posted with incorrect values, which causes ModelState.IsValid to be false, then I have to return the same view with currently posted model. This forces me to get my list of categories again, since I was doing that in the GET action. This adds a lot of duplicated code in controller and I want to remove it. Currently I am doing the following:
My model and view models:
Model, entity stored in the database:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<Category> SubCategories { get; set; }
}
View models:
public class CategoryModel
{
public int Id { get; set; }
public string Name { get; set; }
}
public class TestModel
{
[Required]
[MaxLength(5)]
public string Text { get; set; }
public int SelectedCategory { get; set; }
public IEnumerable<CategoryModel> Categories { get; set; }
public SelectList CategoriesList
{
get
{
var items = Categories == null || !Categories.Any()
? Enumerable.Empty<SelectListItem>()
: Categories.Select(c => new SelectListItem
{
Value = c.Id.ToString(),
Text = c.Name
});
return new SelectList(items, "Value", "Text");
}
}
}
My controller:
public class HomeController : Controller
{
private readonly Repository _repository = ObjectFactory.GetRepositoryInstance();
public ActionResult Index()
{
var model = new TestModel
{
Categories = _repository.Categories.Select(c => new CategoryModel
{
Id = c.Id,
Name = c.Name
})
};
return View(model);
}
[HttpPost]
public ActionResult Index(TestModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Succes");
}
model.Categories = _repository.Categories.Select(c => new CategoryModel
{
Id = c.Id,
Name = c.Name
});
return View(model);
}
public ActionResult Succes()
{
return View();
}
}
I want to remove duplicated Categories fetching and mapping, basically this code:
.Categories = _repository.Categories.Select(c => new CategoryModel
{
Id = c.Id,
Name = c.Name
})
from controller. Also I want to remove ModelState validity check, I want to execute the action only if ModelState.IsValid to keep controller code AS CLEAN AS POSSIBLE. So far I have the following solution for removing ModelState validity check:
Create custom ValidateModelAttribute
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var viewData = filterContext.Controller.ViewData;
if(viewData.ModelState.IsValid) return;
viewData.Model = filterContext.ActionParameters["model"];
filterContext.Result = new ViewResult
{
ViewData = viewData,
};
}
}
Now model is validated before the action executes. In case of validation errors, we use same view with the same recently posted model. Therefore, the controller POST action looks like this:
[HttpPost]
[ValidateModelAttribute]
public ActionResult Index(TestModel model)
{
// Do some important stuff with posted data
return RedirectToAction("Success");
}
This is nice, but now my Categories property of my TestModel is empty, because I have to fetch the categories from the database, and map them accordingly. So is it OK to modify my view model to look something like this:
public class TestModel
{
private readonly Repository _repository = ObjectFactory.GetRepositoryInstance();
...
public int SelectedCategory { get; set; }
public IEnumerable<CategoryModel> Categories {
get
{
return _repository.Categories.Select(c => new CategoryModel
{
Id = c.Id,
Name = c.Name
});
}
}
...
}
This will allow us to have very clean controller, but wouldn't it cause some kind of performance or architectural issues? Wouldn't it break the single responsibility principle for view models? Should ViewModels be responsible for fetching data it needs?
It's not ok. the view model should be mainly a DTO populated by a service/query or even the controller. There was no problem with the previous version, your controller is just a couple of lines of code.
But your repository is not really a repository, it's a an ORM. A proper repository (well here it would be just some query object) would return directly the list of Categories for the view model.
About your auto validation attribute, don't reinvent the wheel, someone else (in this case me) did it before .
No, you shouldn't put repository reference and logic into the view models. I suppose the only thing you need is to be able to rebuild the model if the validation fails. You can try one of the automated ModelState validation, for example:
http://benfoster.io/blog/automatic-modelstate-validation-in-aspnet-mvc
There are a few flows I can see with your approach,
Using the repository and doing actual querying in the controller,
var model = new TestModel
{
Categories = _repository.Categories.Select(c => new CategoryModel
{
Id = c.Id,
Name = c.Name
})
};
Better approach is either to move this to repository or even better put it into a more logical level such as services.
With your new solution it's even worse as you refer the repository inside the view model. Ideally I'd do it like this,
public class TestService : ITestService{
private IReposotory repo;
public TestService(IReposotory repo){
this.repo = repo;
}
public TestModel GetModel()
{
return new TestModel()
{
Categories = _repository.Categories.Select(c => new CategoryModel
{
Id = c.Id,
Name = c.Name
})
};
}
}
public class HomeController : Controller
{
private readonly ITestService _service;
public HomeController (ITestService service){
_service = service;
}
public ActionResult Index()
{
return View(_service.GetModel());
}
[HttpPost]
public ActionResult Index(TestModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Succes");
}
return View(model);
}
public ActionResult Succes()
{
return View();
}
}
I would like to follow best MVC best practise for creating DropLists.
I have 3 Models (I have cut them down for the purposes of this)
Model One
Student
public int ID {get;set;}
public string Name {get;set}
public Site SiteID {get;set;}
Model Two
Site
public int ID {get;set;}
public string SiteName {get;set}
Model Three
VM
public int ID {get;set}
public student Students {get;set;}
public DateTime Date { get { return DateTime.Now; } }
public bool Criteria {get;set;}
In my VM view I am using EditorFor html helpers to populate my VM and Student Models. The site model is pre populated at the database seed.
I am looking for the best way to include a dropdownlist of sites on my VM view, that will map to my student model.
How to I correctly set up my models to achieve this?
In short, you want the DropDownListFor extension method and to put a List<Site> into the view model.
Here is a Fiddle that demonstrates your case. The Fiddle has more details. The nuts and bolts are here:
ViewModel - Add a List<Site>
public class MyViewModel
{
public MyViewModel()
{
this.Sites = new List<Site>();
}
public int ID { get; set;}
public Student Students { get; set; }
public DateTime Date { get { return DateTime.Now; } }
public bool Criteria { get; set; }
public List<Site> Sites { get; set; }
}
View - Use DropDownListFor
#Html.DropDownListFor(m => m.Sites,
new SelectList(Model.Sites, "ID", "SiteName"))
In psuedo-code, the above says
The Sites object in the model contains the properties to display.
Create a new SelectList using the Sites object in the model. Use the ID property as the data value and the SiteName property as the data text.
Create a drop down list based on the above info.
Controller
This just passes a seeded view model to the view.
public ActionResult Index()
{
var vm = SeedFromDatabase();
return View(vm);
}
private MyViewModel SeedFromDatabase()
{
var vm = new MyViewModel();
vm.Sites.Add(new Site(0, "one"));
vm.Sites.Add(new Site(1, "two"));
vm.Sites.Add(new Site(2, "three"));
return vm;
}
ViewModel
public class VM
{
public int ID {get;set}
public student Students {get;set;}
public SelectList SiteList {get;set;}
public int SiteID {get;set;}
public DateTime Date { get { return DateTime.Now; } }
public bool Criteria {get;set;}
}
Load View Action
public ActionResult LoadVMView(){
var model = new VM();
var items = GetSitesFromDatabase().Select(s => new SelectListItem(){
Text = s.SiteName,
Value = s.ID.ToString()
});
model.SiteList = new SelectList(items);
return View(model);
}
View:
#Html.DropDownListFor(m => m.SiteID, Model.SiteList)
On Post
[HttpPost]
public ActionResult LoadVMView(VM model){
var selecteSiteID = model.SiteID;
}
I have a custom model like this:
public class VoteOptionModel
{
public List<int> Index { get; set; }
public List<string> Description { get; set; }
}
And i have a method returns me a datatable.I want to bind database values to my custom model.
var filed1= dt.AsEnumerable().Select(s => s.Field<int>("field1")).ToList();
var field2= dt.AsEnumerable().Select(s => s.Field<string("field2")).ToList();
VoteOptionModel model= new VoteOptionModel();
model.Index = visindex;
model.Description = description;
That code is ok but i'm wondering if there is a "better" way. Can we use AsEnumarable() method?
dt.AsEnumarable().Select(r=>new VoteOptionModel{Index=r["filed1"].toList()}); or sth.
You can gather all aggregated data in one run (make sure you initialize lists in model constructor) with Enumerable.Aggregate:
var model = dt.AsEnumerable().Aggregate(new VoteOptionModel(),
(model,r) => {
model.Index.Add(r.Field<int>("field1"));
model.Description.Add(r.Field<string>("field2"));
return model;
});
If you don't want to change model's constructor, then initialize model this way:
new VoteOptionModel() {
Index = new List<int>(),
Description = new List<string>()
}
But I would suggest to have list of models instead of aggregated model, thus you have pairs of index and description values which are strongly related to each other:
public class VoteOptionModel
{
public int Index { get; set; }
public string Description { get; set; }
}
And getting them this way:
var models = dt.AsEnumerable().Select(r => new VoteOptionModel {
Index = r.Field<int>("field1"),
Description = r.Field<string>("field2")
}).ToList();
I’ve searched for this for a number of hours, but can’t find what I’m looking for answer wise. I have the following:
(Repository.cs)
public class AppRespository
{
private DataContext db = new DataContext();
public IEnumerable<SelectListItem> LoadStates()
{
var query = from d in db.States.ToList()
select new SelectListItem
{
Value = d.StateID.ToString(),
Text = d.State.ToString()
};
return query;
}
}
DataContext.cs
public class DataContext : DbContext
{
public DbSet<States> States { get; set; }
}
ViewModel (States.cs)
public class States
{
[Key]
public int StateID { get; set; }
public IEnumerable<SelectListItem> State { get; set; }
}
Controller (ApplicantController.cs)
// GET: /Applicant/
public ActionResult Index()
{
AppRespository repo = new AppRespository();
States viewModel = new States();
viewModel.State = repo.LoadStates();
return View(viewModel);
}
View:
#Html.DropDownListFor(model=>model.StateID, Model.State, "select")
Why is it when debugging my application I get a null on this line from my repository class?
Text = d.State.ToString(),
State is always null, while StateID is always getting set. I have successfully loaded this drop down before using just my model and controller, but I like this pattern. I'm intermediate w/ ASP MVC so I'm probably missing something.
Any help is appreciated.
Looks like you need to move things around a bit here. I'm making some assumptions about what you want to achieve, but
(Repository.cs)
public class AppRespository
{
private DataContext db = new DataContext();
public IEnumerable<SelectListItem> LoadStates()
{
var query = from d in db.States.ToList()
select new SelectListItem
{
Value = d.StateID.ToString(),
Text = d.State
};
return query;
}
}
DbContext
public class DataContext : DbContext
{
public DbSet<StateEntity> States { get; set; }
}
State Entity
public class StateEntity
{
[Key]
public int StateID { get; set; }
public string State { get; set; }
}
ViewModel
public class StateModel
{
public IEnumerable<SelectListItem> States {get; set;}
}
Controller Action
// GET: /Applicant/
public ActionResult Index()
{
AppRespository repo = new AppRespository();
StateModel viewModel = new StateModel();
viewModel.States = repo.LoadStates();
return View(viewModel);
}
Hopefully this helps get you most of the way there. I'm assuming you want to just load a list of int / string pairs with the StateEntity
Try this
#Html.DropDownListFor(n => n.StateID,
new SelectList(State, "Value", "Text"))
Try changing your query. Instead of:
public IEnumerable<SelectListItem> LoadStates()
{
var query = from d in db.States.ToList()
select new SelectListItem
{
Value = d.StateID.ToString(),
Text = d.State.ToString()
};
return query;
}
Have this:
public IEnumerable<SelectListItem> LoadStates()
{
var query = from d in db.States
select new
{
StateID = d.StateID,
State = d.State
};
var result = from q in query.ToList()
select new SelectedListItem
{
Value = d.StateID.ToString(),
Text = d.State.ToString()
}
return result;
}
The first part of the query sets up a query to get the data from the database the way it's stored there. It uses an anonymous class with 2 properties: StateID and State.
Once the query is ready, we call query.ToList(), which actually runs the query and gets the data, and then makes a list of our anonymous class objects.
Now we can select from the list and convert the data from the anonymous objects to SelectedListItem using .ToString() or any other .NET method.