What are good strategies for rebuilding/enriching a nested or complex ViewModel?
A common way to rebuild a flat ViewModel is shown here
But building and rebuilding a nested ViewModel using that method is too complex.
Models
public class PersonInfo
{
public int Id { get; set; }
public string Name { get; set; }
public int Nationality { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address
{
public int AddressTypeID { get; set; }
public string Country { get; set; }
public string PostalCode { get; set; }
}
public class AddressType
{
public int Id { get; set; }
public string Description { get; set; }
}
view models
public class PersonEditModel
{
public int Id { get; set; }
public string Name { get; set; } //read-only
public int Nationality { get; set; }
public List<AddressEditModel> Addresses { get; set; }
public List<SelectListItem> NationalitySelectList { get; set; } //read-only
}
public class AddressEditModel
{
public int AddressTypeId { get; set; }
public string AddressDescription { get; set; } //read-only
public string Country { get; set; }
public string PostalCode { get; set; }
public List<SelectListItem> CountrySelectList { get; set; } //read-only
}
actions
public ActionResult Update(int id)
{
var addressTypes = service.GetAddressTypes();
var person = service.GetPerson(id);
var personEditModel= Map<PersonEditModel>.From(person);
foreach(var addressType in addressTypes)
{
var address = person.Addresses.SingleOrDefault(i => i.AddressTypeId == addressType.Id)
if(address == null)
{
personEditModel.Addresses.Add(new AddressEditModel
{
AddressTypeId = addressType.Id
});
}
else
{
personEditModel.Addresses.Add(Map<AddressEditModel>.From(address));
}
}
EnrichViewModel(personEditModel, person, addressTypes); //populate read-only data such as SelectList
return Index(personEditModel);
}
[HttpPost]
public ActionResult Update(PersonEditModel editModel)
{
if(!ModelState.IsValid)
{
var person = service.GetPerson(editModel.Id);
var addressTypes = service.GetAddressTypes();
EnrichViewModel(editModel, person, addressTypes);
return View(editModel);
}
service.Save(...);
return RedirectToAction("Index");
}
//populate read-only data such as SelectList
private void EnrichViewModel(PersonEditModel personEditModel, Person person, IEnumerable<AddressType> addressTypes)
{
personEditModel.Name = person.Name;
personEditModel.NationalitySelectList = GetNationalitySelectList();
foreach(var addressEditModel in personEditModel.Addresses)
{
addressEditModel.Description = addressTypes.Where(i => i.Id = addressEditModel.AddressTypeId).Select(i => i.Description).FirstOrDefault();
addressEditModel.CountrySelectListItems = GetCountrySelectList(addressEditModel.AddressTypeId);
}
}
My code for building and rebuilding the ViewModels (PersonEditModel and AddressEditModel) is too ugly. How do I restructure my code to clean this mess?
One easy way is to always build a new view model instead of merging/rebuilding since MVC will overwrite the fields with the values in ModelState anyway
[HttpPost]
public ActionResult Update(PersonEditModel editModel)
{
if(!ModelState.IsValid)
{
var newEditModel = BuildPersonEditModel(editModel.Id);
return View(newEditModel);
}
but I'm not sure that this is a good idea. Is it? Are there other solutions besides AJAX?
I'm going to tackle your specific pain points one-by-one and I'll try to present my own experience and likely solutions along the way. I'm afraid there is no best answer here. You just have to pick the lesser of the evils.
Rebuilding Dropdownlists
They are a bitch! There is no escaping rebuilding them when you re-render the page. While HTML Forms are good at remembering the selected index (and they will happily restore it for you), you have to rebuild them. If you don't want to rebuild them, switch to Ajax.
Rebuilding Rest of View Model (even nested)
HTML forms are good at rebuilding the whole model for you, as long as you stick to inputs and hidden fields and other form elements (selects, textarea, etc).
There is no avoiding posting back the data if you don't want to rebuild them, but in this case you need to ask yourself - which one is more efficient - posting back few extra bytes or making another query to fetch the missing pieces?
If you don't want to post back the readonly fields, but still want the model binder to work, you can exclude the properties via [Bind(Exclude="Name,SomeOtherProperty")] on the view model class. In this case, you probably need to set them again before sending them back to browser.
// excluding specific props. note that you can also "Include" instead of "Exclude".
[Bind(Exclude="Name,NationalitySelectList")]
public class PersonEditModel
{
...
If you exclude those properties, you don't have to resort to hidden fields and posting them back - as the model binder will simply ignore them and you still will get the values you need populated back.
Personally, I use Edit Models which contain just post-able data instead of Bind magic. Apart from avoiding magic string like you need with Bind, they give me the benefits of strong typing and a clearer intent. I use my own mapper classes to do the mapping but you can use something like Automapper to manage the mapping for you as well.
Another idea may be to cache the initial ViewModel in Session till a successful POST is made. That way, you do not have to rebuild it from grounds up. You just merge the initial one with the submitted one in case of validation errors.
I fight these same battles every time I work with Forms and finally, I've started to just suck it up and go fully AJAX for anything that's not a simple name-value collection type form. Besides being headache free, it also leads to better UX.
P.S. The link you posted is essentially doing the same thing that you're doing - just that its using a mapper framework to map properties between domain and view model.
Related
B"H
Is there a way to return field names when using Tuples as return types for actions?
What I would like to do is skip creating DTOs for every single function in every single controller. When I have a complex system with many controllers each with many actions (functions). I often find that there are a handful of central DTOs. Then there are hundreds of slight variations of them. One for each function. I would like to stick with the handful of central classes and skip the extra classes. Replacing them with Tuples.
For example. I have a Customer class
public class Customer
{
public string Name{ get; set; }
public string Email{ get; set; }
}
with a many to many relation to store locations
public class StoreLocation
{
public string Name{ get; set; }
public string City{ get; set; }
public string State{ get; set; }
public string Focus{ get; set; }
}
I then have a function in a controller
[HttpGet("TopCustomersByState")]
public IEnumerable<(Customer customer, string state )> TopCustomersByState()
{
}
and a function
[HttpGet("TopCustomersByFocus")]
public IEnumerable<(Customer customer, string focus)> TopCustomersByState()
{
}
and a function
[HttpGet("CustomersAndTotalMoneySpent")]
public IEnumerable<(Customer customer, float moneySpent)> CustomersAndTotalMoneySpent()
{
}
These function are all accessed from Javascript in the browser expecting JSON.
Until now, I'd make a separate class for each return type. This quickly gets out of hand.The solution that I present above in my examples if it were to work would be perfect.The issue is that the JSON being returned is
{
"item1": {
},
"item2": null
}
instead of the property names customer, moneySpent, etc. as you'd expect.
Thank you
You don't need to put any return type of data at all if you want to make it generic. In any case it is converted to a json string. You can use an anonymous class for example
[HttpGet("CustomersAndTotalMoneySpent")]
public IActionResult CustomersAndTotalMoneySpent()
{
... your code
return Ok (new { Focus = focus, Customer = customer } );
//or
return Ok (new List<object> { { Focus = focus, Customer = customer} });
//or List<dynamic>
}
Fairly new to EF.Core and I'm having some issues as my tables start getting more complex. Here's an example of what I have defined for my classes. Note ... there are many more columns and tables than what I have defined below. I've paired them down for brevity.
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Active { get; set; }
}
Followed by
public class JournalEntry
{
public int Id { get; set; }
public int UserId { get; set; }
public string Details { get; set; }
public DateTime DateEntered { get; set; }
public virtual User User { get; set; }
}
I want to be able to issue the following query and INCLUDE the User Table so that I can then populate a ViewModel with columns from the User Table without having to do another lookup and also to sort the data while retrieving it:
public IQueryable<JournalEntry> GetByUser(int userId)
{
return _DbContext.JournalEntries.Where(j => j.UserId == userId)
.Include(u => u.User)
.OrderBy(u=> u.User.FirstName)
.ThenBy(j => j.DateEntered);
}
My controller would then have something similar to the following:
public IActionResult List(int userId)
{
var journalEntries = new _dbRepository.GetByUser(userId);
var myViewModel = new MyViewModel();
myViewModel.UserName = ($"{journalEntries.User.FirstName} {journalEntries.User.LastName}");
myViewModel.Entries = journalEntries;
etc ....
return View(myViewModel);
}
I'm loading the user's first and last name in the View Model and whatever other attributes from the various tables that are referenced. The problem that I'm having is that I'm getting errors on the Migration creation "Foreign key constraint may cause cycle or multiple cascade paths." And of course, if I remove the line reading public virtual User User { get; set; } from the JournalEntry class then the problem goes away (as one would expect).
I believe that the way I'm doing the models is incorrect. What would be the recommended way that I should code these models? I've heard of "lazy loading". Is that what I should be moving towards?
Thanks a bunch.
--- Val
Your query returns an IQueryable<JournalEntry> not a JournalEntry.
Change the code to get the user details from the first object:
var myViewModel.UserName = ($"{journalEntries.First().User.FirstName} {journalEntries.First().User.LastName}");
In the line above I'm calling First() on your journal entries collection and that would have a User. Then I can access FirstName and LastName.
Also, don't bother with LazyLoading since you are learning. It could cause select n+1 issues if used incorrectly
I'm listening to "push" notifications coming into my server. I've set up SubscriptionModel with all possible properties, and I can correctly iterate through the JSON body coming through, parse each Subscription, and modify the output before returning the list I created. However, I'd like to know how I might go about removing properties of SubscriptionModel when I don't need to return them at all; or removing them if they're null before responding back with List<SubscriptionModel> subscriptions.
namespace TextMessagingListener.Controllers
{
public class SubscriptionModel
{
public long push_id { get; set; }
public string request_id { get; set; }
public string subscription_id { get; set; }
public string message { get; set; }
public string status_code { get; set; }
public string error_message { get; set; }
}
[Route("api/[controller]")]
public class SubscriptionController : Controller
{
// PUT api/subscription
[HttpPut]
public List<SubscriptionModel> Put([FromBody] List<SubscriptionModel> model)
{
// Receive a report of whether your subscription(s) was successfully added or not.
List<SubscriptionModel> subscriptions = new List<SubscriptionModel>();
foreach (SubscriptionModel m in model)
{
m.message = "Push notification successfully received.";
subscriptions.Add(m);
}
return subscriptions;
}
}
}
The only solution I can think of is to create another object which will just be for returning information; and applying each subscriptions item I want to send on to that.
You can't. You'd need another class. A "light" version that contains just the properties. Or you could do an anonymous type, but that is difficult to work with. I agree with the other guy on your naming conventions though :).
If I have a complex model representing a large amount of data, and I only wish to display a cut-down version of that model (e.g. Name, Description), what is the best approach in MVVM?
Most solutions I can find seem to assume that the data is already present in memory and recommend using a new ViewModel that exposes only the fields required.
However rather than select out all of the data from the database, it would be preferable to select just what is necessary. Do I then create a new model to hold that data? Selecting directly into the ViewModel is possible but feels like the wrong thing to do. Likewise using a new model to represent a different version of the same data also feels off somehow.
What is the accepted method of doing this?
As a simple example (Simple enough class that I wouldn't ordinarily do this):
public class User {
public int UserID {get;set;}
public string FirstName
public string LastName
public int AccessLevelID
public List<Groups> UserGroups
}
but I only really need:
public class PreviewUser {
int UserID
string FirstName
}
You can create another type with is a subset of the business type.
usually this is known as a
DTO - Data transfer Object which encapsulates only what you need. so the database needs to query only the subset of the entity.
public class UserDto
{
public int ID { get;set;}
public string Name{ get;set;}
}
Secondly if you need to add some ui logic to the display it is common to wrap the specific DTO in a more specific UI model.
public class UserUI
{
UserDTO _userDto;
UserUI(UserDTO userDto)
{
_userDto = userDto;
}
public string Name
{
get{return IsAfter_21_hours ? "The user as gone home" : _userDto.Name;}
}
}
the UserViewModel will reference an instance of UserUI.
You can either remove properties you don't need from the model (to slightly improve performance) or you can create a viewmodel that will provide only properties that you want to show.
Here is an example:
public class UserViewModel
{
private readonly User _user;
public UserViewModel(User user)
{
_user = user;
}
public int UserID
{
get { return _user.UserID; }
}
public string FirstName
{
get { return _user.FirstName; }
}
}
...
var viewModels = userRepository.GetUsers().Select(user => new UserViewModel(user));
UPDATED:
If performance is really important for you, you can use inheritance. Base class will be smaller version of the data and derived class will contain complete data. You can use the base class when you need to get only some fields from DB and save bandwidth.
public class BaseUser
{
public int UserID { get; set; }
public string FirstName { get; set; }
}
public class User : BaseUser
{
public string LastName { get; set; }
public int AccessLevelID { get; set; }
public List<Groups> UserGroups { get; set; }
}
There are number of approaches you may use:
use "full version" of source model. Since you're building UI, the user will see only what you want to display;
use view model, and wrap source model into this view model. The implementation is trivial, and amount of data is limited before UI;
use view model, and copy data from source model into this view model. The implementation is more complex (either mapping from existing model, or loading only required data from database), but view model and model are totally decoupled.
Actually, it depends on what is more suitable for you.
Note, that often the difference between "view model" and "model" is blurred. If the model looks like this:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
...and view model - like this:
public class PersonViewModel
{
public int Id { get; set; }
public string Name { get; set; }
}
then throw this view model away. While there's no difference, you don't need to create extra classes.
I think you don't have to create new model class to hold user data for view . Instead create a view model class and map the model properties to VM. See the example below
public class UserViewModel
{
Public UserViewModel(User user)
{
//initialize required viewmodel properties here
}
int UserID {get;set;}
string FirstName{get;set;}
}
I have a model:
public class DataModel
{
public GridSortOptions SortOptions { get; set; }
public string Term { get; set; }
public int? Page { get; set; }
...
}
public class GridSortOptions
{
public string Column { get; set; }
public SortDirection Direction { get; set; }
}
And a http request: ?Column=LastName&Direction=Descending
That request will not work, i need to update it as follow: ?SortOptions.Column=LastName&SortOptions.Direction=Descending
Is it possible to teach ASP.NET to understand the original request without changing the model?
PS. I know that I could create a custom binder but I feel that there is a much simple way exist...
Thanks in advance!
I have mine structured slightly differently, as I came across the same problem (in MVC1 and still have it now - mvc4) . I have often found that having nested models tend to cause headaches and unnecessary complexity, so I try to flatten my view models as much as possible, just to simplify things, so don't have to write custom model binders, saves a lot of time and code.
My action typically looks method looks like this
//...snip
public ActionResult List(int? page, GridSortOptions sortOptions, string keyword) {
var model = new UserGridViewModel();
IQueryable<User> users = new UserRepository(Session).List();
if (sortOptions.Column != null) {
users = users.OrderBy(sortOptions.Column, sortOptions.Direction);
}
if (keyword != null) {
users = users.Where(x => x.Name.Contains(keyword))
}
model.SortOptions = sortOptions;
//using MvcContrib.Pagination.PaginationHelper here
model.Results = users.AsPagination(page ?? 1, 20);
return View(model);
}
//.....
My view logic is simply:
#using MvcContrib.UI.Grid
#using MvcContrib.UI.Pager
#model UsersGridViewModel
#Html.Grid(Model.Results).Columns(cols => {
cols.For(col => col.Id).Sortable(true);
cols.For(col => col.Name).Sortable(true);
//...etc
}).Sort(Model.SortOptions)
My grid view models are normally like this:
public class UserGridViewModel
{
public IPagination<User> Results { get; set; }
public GridSortOptions SortOptions { get; set; }
}