ASP.NET MVC4 with EF 6 Model Update Issue - c#

I have two models as below
public class Category
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int ID { get; set; },
[Required]
public string category { get; set; }
[Required]
public string Desc { get; set; }
}
public class Product
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int ID { get; set; },
public int CatID { get; set; },
[ForeignKey("CatID")]
public virtual Category Category { get; set; },
[Required]
public string Desc { get; set; },
public string DisplayName
{
get
{
return string.format("{0} - {1}",this.Category.category,this.Desc);
}
}
}
This is my Edit Action
public ActionResult Edit(int id)
{
ViewBag.PossibleCategories = categoryRepository.All;
return View(productRepository.Find(id));
}
[HttpPost]
public ActionResult Edit(Product product)
{
if (ModelState.IsValid) //<== This becomes false saying category.desc is required
{
productRepository.InsertOrUpdate(product);
productRepository.Save();
return RedirectToAction("Index");
}
else
{
ViewBag.PossibleCategories = categoryRepository.All;
return View();
}
}
I have a scaffolded a Edit view of product and it shows ID and DisplayName as Readonly. All the other fields a editable.
The edit view also has the product -> category -> category has a read-only text field
#Html.TextBoxFor(model => model.Category.category, new Dictionary<string, object>() { { "readonly", "true" } })
The Post back sends this and tries to create a new category. This is not required. The category link will be carried forward using the product.CatID.
How can i display these types of fields??
When the Edit view Post back the Model state appears as invalid because the product's category's desc is null (product -> category -> desc).
if i comment out the DisplayName property in Product this issue doesn't occur.
From my understanding, this is because the DiaplayName property refers to Category property and the view view doesn't have category.desc field so when the model is created back on the POST action, the desc is not populated. Adding the category.desc field to the view is one way of solving this problem.
Is there any other method to solve this?
Note: This is not the only model i'm having this issue. There are many complex models which have the same problem and to me having these fields also included in the view would make for (1) a very cluttered view (2) the amount of data making the round trip will be high.

Simple Solution
Check for null. Really you should be making this a habit anyway.
public string DisplayName
{
get
{
if(this.Category != null)
{
return string.format("{0} - {1}",this.Category.category,this.Desc);
}
else
{
return String.Empty;
}
}
}
Complex Solution
Instead of directly using your database model in your Views another solution is to create ViewModels. These are models meant specifically for your View. As a simplified example, let's take your Product model and create a ViewModel.
Create a folder for your ViewModels
Create ViewModel files that match your Controller
Create a ViewModel that you will use in your View
Say you have a Store Controller. This would be the file structure you would create.
Models
ViewModels
StoreViewModels.cs
Inside the StoreViewModels you would create a ViewModel called ProductViewModel which you would fill in with information from Product.
public class ProductViewModel
{
public int ID { get; set; }
public string Description { get; set; }
public string DisplayName { get; set; }
public ProductViewModel() { }
public ProductViewModel(Product product)
{
this.ID = product.ID;
this.Description = product.Description;
this.DisplayName = product.DisplayName;
}
}
In your View you reference ProductViewModel instead of Product. On the receiving end you then translate the ViewModel fields back to your Model. Let me know if you have any questions.

Related

How can I use Automapper to assign values to fields in destination model but not in source model?

I am getting the hang of Automapper in an ASP.NET MVC 5 application for the purpose of mapping a domain model to a ViewModel. There is a case that I still don't know how to resolve: when the ViewModel (destination) has a property not in the domain model (source).
The two additional properties in the ViewModel are IEnumerables that I need to populate in the Controller.
As I explain in the comments in the Controller block (shown below), the domain model is the source and will be fed into the View table. The additional two IEnumerables in the ViewModel will fill the DropDownLists in the HTML.BeginForm() block.
The examples I have seen using .CreateMap<>().ForMember() deal with calculations or transformations of properties in the source model, and not this case, where I am defining something in the controller based on the Action parameters.
My question is how to map the remaining IEnumerables, as defined in the controller?
Mapping Config in App_Start
public static class MappingConfig
{
public static void RegisterMaps()
{
AutoMapper.Mapper.Initialize(config =>
{
config.CreateMap<StudentRoster, StudentRosterViewModel>();
});
}
}
Model and ViewModel:
[Table("StudentRoster")]
public partial class StudentRoster
{
public int ID { get; set; }
[Required]
[StringLength(3)]
public string Campus { get; set; }
[Required]
[StringLength(4)]
public string FiscalYear { get; set; }
[Required]
[StringLength(50)]
public string StudentName { get; set; }
public int StudentID { get; set; }
}
// ViewModel
public partial class StudentRosterViewModel
{
// Automapper successfully mappped the first five fields
// to the parent class
public int ID { get; set; }
public string Campus { get; set; }
public string FiscalYear { get; set; }
public string StudentName { get; set; }
public int StudentID { get; set; }
// These two fields are not in the parent class
public IEnumerable<SelectListItem> CampusListSelect { get; set; }
public IEnumerable<SelectListItem> FiscalYearSelect { get; set; }
}
Index Action in Controller:
// GET: StudentRosterViewModels
public ActionResult Index(string campus = "MRA", string fy="FY16")
{
IEnumerable<StudentRoster> query = db.StudentRosters.Where(m=>m.Campus==campus).ToList();
// This successfully maps the domain model to the viewmodel
// This IEnumerable will display in the "Table"
IEnumerable<StudentRosterViewModel> mappedQuery =
AutoMapper.Mapper.Map<IEnumerable<StudentRoster>, IEnumerable<StudentRosterViewModel>>(query);
// The two remaining IEnumerables need to be mapped to 'mappedQuery'
// CampusListSelect and FiscalYearSelect
// These two IEnumerables will populate the dropdownlists in Html.BeginForm()
IEnumerable<SelectListItem> CampusList = new SelectList(new List<string> { "CRA", "DRA", "MRA", "PRA" }, campus);
IEnumerable<SelectListItem> FiscalYearList = new SelectList(new List<string> { "FY12", "FY13", "FY14", "FY15", "FY16" }, fy);
return View(mappedQuery.ToList());
}
You can try to use DynamicMap to populate your items without creating additional classes for mapping.
In case if you're using the old version of AutoMapper (4.1 or below) the you can try something the following:
// GET: StudentRosterViewModels
public ActionResult Index(string campus = "MRA", string fy="FY16")
{
IEnumerable<StudentRoster> query = db.StudentRosters.Where(m=>m.Campus==campus).ToList();
// This successfully maps the domain model to the viewmodel
// This IEnumerable will display in the "Table"
IEnumerable<StudentRosterViewModel> mappedQuery =
AutoMapper.Mapper.Map<IEnumerable<StudentRoster>, IEnumerable<StudentRosterViewModel>>(query);
// The two remaining IEnumerables need to be mapped to 'mappedQuery'
// CampusListSelect and FiscalYearSelect
// These two IEnumerables will populate the dropdownlists in Html.BeginForm()
IEnumerable<SelectListItem> CampusList = new SelectList(new List<string> { "CRA", "DRA", "MRA", "PRA" }, campus);
IEnumerable<SelectListItem> FiscalYearList = new SelectList(new List<string> { "FY12", "FY13", "FY14", "FY15", "FY16" }, fy);
var objForDynamicMapping = new
{
CampusListSelect = CampusList,
FiscalYearListSelect = FiscalYearList
};
foreach(var mappedItem in mappedQuery)
{
// will create the mapping configuration dynamically
AutoMapper.Mapper.DynamicMap(objForDynamicMapping, mappedItem);
}
return View(mappedQuery.ToList());
}
In case if you're using the AutoMapper 4.2 or high.
Then you just need to put this row:
var config = new MapperConfiguration(cfg => cfg.CreateMissingTypeMaps = true);
in place where you create the mapper configuration and then just use method Map like:
mapper.Map(objForDynamicMapping, mappedItem);
instead of DynamicMap.
Hope it will help.

ViewModel becomes null when passed between actions

I'm having trouble passing a viewmodel into a view. I have two views: a Search view and a GeneralForm view. Search passes search results into GeneralForm's viewmodel.
Say the GeneralForm is a complex viewmodel that holds two other viewmodels:
public class GeneralFormViewModel
{
public GeneralInfoViewModel GeneralInfo { get; set; }
public NotesViewModel Notes { get; set; }
}
public class GeneralInfoViewModel
{
[Required(ErrorMessage = "Please enter the person's name.")]
[DisplayName("Name:")]
public string Name { get; set; }
[Required(ErrorMessage = "Please enter the person's ID.")]
[DisplayName("ID:")]
public int ID { get; set; }
}
public class NotesViewModel
{ // etc.
(I set up this way in order to use multiple #Html.BeginForms on my GeneralForm view. In this way, I hope to POST and validate small sections of the entire general form, one at a time, using KnockoutJS and AJAX.)
[HttpPost]
public ActionResult Search(SearchViewModel vm)
{
var query = // do some searching
var viewmodel = new GeneralFormViewModel()
{
GeneralInfo = new GeneralInformationViewModel
{
ID = query.id,
Name = query.name
}
};
return RedirectToAction("GeneralForm", viewmodel);
}
At this point, viewmodel.GeneralInfo is not null, and the viewmodel is passed to the GeneralForm controller.
[HttpGet]
public ActionResult GeneralForm(GeneralFormViewModel model)
{
return View(model);
}
Now model.GeneralInfo is null. What conventions of MVC am I breaking by doing this, and how can I get the GeneralForm view to render the data acquired via the Search controller to the GeneralForm view?
Problem is You can't send data with a RedirectAction.
you're doing a 301 redirection and that goes back to the client.
Store it in a TempData or Session ...
See the following post:
passing model and parameter with RedirectToAction

Rebuilding a nested ViewModel on !ModelState.IsValid

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.

#Html.DropDownList returns null when submitted

I have here a scenario. I want to make an HTTP POST action in the form so here's how I did it.
public class Item
{
public Item()
{
Storages = new HashSet<Storage>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Storage> Storages { get; set; }
-- remove some lines for brevity --
}
public class Storage
{
public int Id { get; set; }
public string Name { get; set; }
--- remove some lines for brevity --
}
So basically, An Item has many Storage And so I created viewmodel.
public class CreateStockViewModel
{
public string Name { get; set; }
public int StorageId { get; set; }
-- remove some lines for brevity --
}
In my Controller. I have this
[HttpGet]
public ActionResult Create()
{
ViewBag.Storages = _storageService.All
.OrderBy(i => i.Name)
.ToSelectList(s => s.Name, s => s.Id);
return View();
}
In my View:
#model Wsfis.Web.ViewModels.ItemViewModels.CreateStockViewModel
#Html.DropDownList("Storages")
Now my problem is, when I submit the form. And have Quick Watch to the model being passed. It is Null or 0
public ActionResult Create(CreateStockViewModel item)
{
// some code
}
In a nutshell,
When I submit the form all fields are being bind except for the #Html.DropDownList. Where did I missed?
Some additional side note:
They say Views should be strongly typed. Then what should I pass in View then? (A sample code would be great. Thanks)
As for the ToSelectList method I copy this code (I hope it's alright)
Any help would be much appreciated. Thanks.
Your form input has a different name to your property so the default model binder doesn't know how to bind your model.
You could pass in a different name to use to the DropDownList helper, however I prefer to use the strongly typed helpers:
#Html.DropDownListFor(m => m.StorageId, ViewBag.Storages as IEnumerable<SelectListItem>)
Try like this:
ViewBag.StorageId = _storageService.All
.OrderBy(i => i.Name)
.ToSelectList(s => s.Name, s => s.Id);
in view:
#Html.DropDownList("StorageId")
it will now post the drop down list selected value in CreateStockViewModel object's StorageId property.

Dynamically binding and creating items with complex database structure to Create and Edit views using viewModel in MVC 3 with EF4

I had bad times trying to figure out how to deal with this. Any help wold be appreciated. Even a suggestion of a better structure that fits to my needs: Construct category items in a category having an specification of how they can be in a category item property list.
This is used to, among other things, dinamicaly generate forms for creating and editing items.
Long story short: I need to know if I'm doing it right or a better (maybe automated) way of deal with it without breaking the whole app.
.
.
I'm working with MySQL 5 in VWD Express 2010 in a Win7 64bit machine with all MySQL drivers intalled (ODBC and .NET specific provider, the last one is not compatible with ASP.Net 4). Other problem rised here, but can be target for a separate question: I'm writing all of my models, 'cause MySql isn't compatible with the Linq to SQL (I can imagine why, but not sure).
.
.
Returning to the real topic:
My models are:
Category - Them main entity, with a name property, a collection of CategoryItemProperty entities and a collection of Item entities;
CategoryItemProperty - An entity with a name and some other properties that dictate how the Items in this category may be (field size, mask, input restriction, etc);
Item - The entity whose properties are based on the category properties;
ItemProperty - The properties of the items (field size, mask, input restriction, etc)
The code is something around this:
public class Category
{
public int CategoryId { get; set }
public string Description { get; set }
//...
public virtual List<CategoryItemProperty> ItemProperties { get; set; }
}
public class CategoryItemProperty
{
public int CategoryItemPropertyId { get; set; }
public string Label { get; set; }
public string Name { get; set; }
public int Size { get; set; }
public int MaxLenght { get; set; }
//...
public virtual Category Category { get; set; }
}
public class Item
{
public int ItemId { get; set; }
public string Description { get; set; }
public int CategoryId { get; set; }
//...
public virtual Category Category { get; set }
public virtual List<ItemProperty> Properties { get; set; }
}
public class ItemProperty
{
public int ItemPropertyId { get; set; }
public int ItemId { get; set; }
public int CategoryItemPropertyId { get; set; }
public string Value { get; set; }
//...
public virtual Item Item { get; set; }
public virtual CategoryItemProperty CategoryItemProperty { get; set; }
}
.
.
The big problem here, with this approach, is to generate the form and deal with the data on the controller side to be saved to the database.
.
.
A more detailed example wold be: Generate a simple contact form:
We create a Category with some field specification:
var category = new Category() { Description = "Simple Contact Form" };
MyEntitySet.Categories.Add(category);
MyEntitySet.SaveChanges();
//...
var name = new CategoryItemProperty() { Label = "Name", Size = 50, MaxLength = 50 };
var message = new CategoryItemProperty() { Label = "Message", Size = 50, MaxLength = 255 };
category.ItemProperties.Add(name);
category.ItemProperties.Add(message);
MyEntitySet.Entry(category).State = EntityState.Modified;
MyEntitySet.SaveChanges();
.
.
What I have came up until now is to create a ViewModel to pass the category info and its item property collection to the Create and Edit views and doing a loop through the ItemProperties to generate the fields and working in the ItemController, receiving the FormCollection and generating the Item and its ItemPropertys objects and saving them to the database. But this process is terrible and painfull:
.
Items/Create View:
#model MyApp.Models.CategoryItemModelView
#Html.EditorFor(m => m.Description);
...
#foreach(var property in Model.ItemProperties)
{
<label>#property.Label</label>
<input type="text" name="#property.Name" size="#item.Size" maxlength="#item.MaxLength" />
}
In the Controller:
// GET: /Items/Create/5
public ActionResult Create(int id)
{
var categoryItemModelView = new CategoryItemModelView();
categoryItemModelView.Populate(id); // this method maps the category POCO to the ViewModel
return View(categoryItemModelView);
}
// POST: /Items/Create/5
[HttpPost]
public ActionResult Create(int id, FormCollection formData)
{
if (ModelState.IsValid)
{
var category = MyEntitySet.Categories.Find(id);
var item = new Item
{
CategoryId = id,
Description = formData["Description"],
// ...
Category = category
};
MyEntitySet.Items.Add(item);
foreach(var property in category.ItemProperties)
{
var itemProperty = new ItemProperty
{
ItemId = item.ItemId,
CategoryItemPropertyId = property.Id,
Value = form[property.Name],
// ...
Item = item,
CategoryItemProperty = property
};
MyEntitySet.ItemProperties.Add(itemProperty);
}
MyEntitySet.SaveChanges();
return RedirectToAction("Index");
}
// Here I don't know how to return the typed data to the user (the form returns empty)
var categoryItemModelView = new CategoryItemModelView(id);
categoryItemModelView.Populate(id); // this method maps the category POCO to the ViewModel
return View(categoryItemModelView);
}
.
.
My problem rises in building the Create and Edit actions and its respective views (see above how I'm doing it right now). How to handle this case, when I have to use the Category.ItemProperties to generate the fields and store the information in an Item object and the field values in its ItemProperty object?
.
.
Please note: All this code is for example purposes only. My code is similar, but its handled by a specific controller and specific views to CRUD Categories and CategoryItemProperties and I have no problem with this.
.
.
Sorry for this long text. I've tryed to be as clearest as I can. If you need any more info, drop a comment, please.
Okay rcdmk! first of all my English is terrible and i'm here to just share with you my few experience.
I have build such a complex software in the past with MVVM(WPF) + EF4 + T4 to generate POCO's and i deal with Microsoft Blend to handle with actions, bindings and so on between the client and the viewmodels!
that's work great! i hope i helped you!
ps:
I dont know if Blend supports ASP.Net but creating POCO(Viewmodel) with lazy loading could help u somewhere!
As i understand , Category and corresponding CategoryPropertyItems is describing how Item will be created . Simply Category is drawing an abstract form and item and item properties are concretes (because item property has Value property). So in Item/Create Action (GET) you can build item and it's properties using Category and CategoryPropertyItems.
public Item Build(Category category)
{
Item item = new Item();
item.Category = category;
item.ItemId = ...
foreach(var categoryItemProperty in category.ItemProperties)
{
ItemProperty itemProperty = new ItemProperty();
itemProperty.Item = item;
itemProperty.CategoryItemProperty = categoryItemProperty;
itemProperty.ItemPropertyId = ...
}
return item;
}
In result of Index/Create Action you can use either just this Item object or you can put item into ViewModel .
In View you can bind direcly to model (Item) properties .
This links can help you .
Editing and binding nested lists with ASP.NET MVC 2
Model Binding To A List

Categories