I'm creating a blog in C# MVC, but I'm having a problem with the blog posts. From a list of posts, only one post is retrieved, and it's the last one. I wanna retrieve all blog posts with their associated images...
BLOG MODELS
public class Blog
{
[Key]
public int BlogID { get; set; }
public string Title { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
[Key]
public int PostID { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogID { get; set; }
public bool hasImages { get; set; }
public virtual Blog Blog { get; set; }
public virtual ICollection<ImageFile> ImageFiles { get; set; }
}
public class ImageFile
{
[Key]
public int ImageID { get; set; }
public string ImageName { get; set; }
public string ImageType { get; set; }
public byte[] ImageBytes { get; set; }
public int PostID { get; set; }
public virtual Post Post { get; set; }
}
METHOD TO RETRIEVE POSTS + IMAGES
public Tuple<IEnumerable<Post>, IEnumerable<ImageFile>> getAllBlogData()
{
var AllPosts = _entities.Posts.ToList();
Tuple<IEnumerable<Post>, IEnumerable<ImageFile>> model = null;
foreach (var post in AllPosts)
{
var posts = AllPosts.Where(e => e.PostID == post.PostID);
if (posts != null)
{
//checks if a blog post has images
if (post.hasImages)
{
var images = _entities.ImageFiles.Where(e => e.PostID == post.PostID);
model = new Tuple<IEnumerable<Post>, IEnumerable<ImageFile>>(posts, images);
}
else
{
model = new Tuple<IEnumerable<Post>, IEnumerable<ImageFile>>(posts, null);
}
}
}
return model;
}
CONTROLLER
public ActionResult Posts()
{
return View(_blogrepository.getAllBlogData());
}
VIEW
#model Tuple<IEnumerable<Winter.Domain.Entities.Post>, IEnumerable<Winter.Domain.Entities.ImageFile>>
#{
ViewBag.Title = "Posts";
}
<hgroup>
<h3>BLOG</h3>
</hgroup>
#foreach (var post in Model.Item1)
{
<p class="container images">
#if (Model.Item2 != null)
{
foreach (var image in Model.Item2)
{
<span>
<img src="data:image/png;base64,#Convert.ToBase64String(image.ImageBytes)" />
</span>
}
<div class="clearfix"></div>
}
<hgroup>
<h2>#Html.DisplayFor(modelItem => post.Title)</h2>
</hgroup>
<span>
#Html.DisplayFor(modelItem => post.Content)
</span>
</p>
}
<hr />
Thanks in advance
Below, I give you 2 ways of making your code work.
The first being the "correct" way to implement what you're trying to do and the second being a fix of the current code you're using.
Get a beverage, and have a good read. note: this is all off the top of my head (so please forgive in advance - just comment anything that needs correcting)
1) The "correct" way to do it:
It looks like you're using Entity Framework. (If not, then why not?)
That said, I would look into Entity Framework Relationships & Navigation Properties.
This is exactly what Database Relationships are for. Your code is currently making heavy work of what is an in-built feature, and you're using Tuples unnecessarily. EF is there to allow you to use real objects you've already created.
Here's a short(ish) way you should update your structure (this example is using EF Code-First - I find it way easier to create the DB from already-made classes that I use around the program):
Classes
public class Post
{
public int Id { get;set; }
public string Title { get; set; }
// Other properties etc.
public virtual ICollection<ImageFile> Images { get; set; }
}
public class ImageFile
{
public int Id { get; set; }
public byte[] ImageBytes { get; set; }
[ForeignKey(nameof(Post))]
public int PostId { get; set; }
[ForeignKey(nameof(PostId))]
public Post Post { get; set; }
}
This gives you a one-to-many relationship of "one post" has "many image files". The virtual property of Images in the Post class means it is a "navigation property" - this means that EF can figure out the relationship between the ImageFile that have the matching PostID to that post. This is down to FluentAPI's convention (TL;DR)
See how Code First Relationships work in Entity Framework for more information.
Controller
Your controller can then return a List<Post> from your context.
public ActionResult Posts()
{
var posts = _yourDbContext.Posts.Include(x => x.Images).ToList();
return View(posts);
}
Note: You will need to include using System.Data.Entity at the top of your file's using statements, in order to use the Include() method with a lambda expression.
Further note: If you don't "flatten" the results to a List<>, then you might not (off the top of my head) need to use the Include() method - TL;DR: when the page is rendered, it will still have the context in scope, and will be able access the navigation properties using the query (IQueryable<>) it still uses (lazy loading). Include() simply uses eager loading to pull the images out and place them in the List<>.
View
Then, in your view you can simply use the #model as an IEnumerable<Post>, iterating over the List/IEnumerable you passed in; referencing the Images property that way.
Like so:
#model IEnumerable<Your.Namespace.Post>
// Other fluff - page title etc.
#foreach (var post in Model)
{
foreach (var image in post.Images)
{
// Display each image - #image.ImageBytes
}
<h1>#post.Title</h1>
// Other #post.Property printing, here
}
Using this way will enable you to (use a lot less code), and use your classes for what they would usually be designed for in OOP.
I have also included a "dirty" answer - using the current method you're using, below:
2) The "dirty" fix of your code:
Similar to #Jauch's answer, but a little more condensed for both the code, and your view:
Your getAllBlogPosts() method is simply returning the last post in the iteration, every time - you're overwriting model each time.
You could simply return a list of blog posts like this:
private List<Tuple<Post, IEnumerable<ImageFile>>> GetAllBlogPosts()
{
var allPosts =
_entities.Posts.Select(
post =>
new Tuple<Post, IEnumerable<ImageFile>>(
post,
_entities.ImageFiles.Where(x => x.PostID == post.PostID)));
return allPosts.ToList();
}
Then, in your view you can simply iterate the .Item2 property (the images), without having to do a null check beforehand.
This is because, even if there are no results, the IEnumerable<ImageFile> will result as simply an empty collection.
In the view, your model will be an IEnumerable<Tuple<Post, IEnumerable<ImageFile>>> ( whereFQNS is the fully qualified namespace of your classes):
#model IEnumerable<Tuple<FQNS.Post, IEnumerable<FQNS.ImageFile>>>
Your view can then iterate over each Tuple in the Model - let's say item.
Then inside the item, you can reference Value1 (being the Post) and Value2 (being the Post's images).
Like so:
#foreach (var item in Model)
{
<p class="container images">
foreach (var image in item.Item2)
{
<span>
<img src="data:image/png;base64,#Convert.ToBase64String(image.ImageBytes)" />
</span>
}
<div class="clearfix"></div>
<hgroup>
<h2>#Html.DisplayFor(x => item.Value1.Title)</h2>
</hgroup>
<span>
#Html.DisplayFor(x => item.Value1.Content)
</span>
</p>
}
I hope that this helps you along the way with your project, whichever method you take.
I would heavily advise to research into EF's relational capabilities and using navigation properties in order to make your code much cleaner (and your life much easier).
Hope this helps! :)
I would say that the problem is in this part of the code:
public Tuple<IEnumerable<Post>, IEnumerable<ImageFile>> getAllBlogData()
{
var AllPosts = _entities.Posts.ToList();
Tuple<IEnumerable<Post>, IEnumerable<ImageFile>> model = null;
foreach (var post in AllPosts)
{
var posts = AllPosts.Where(e => e.PostID == post.PostID);
if (posts != null)
{
//checks if a blog post has images
if (post.hasImages)
{
var images = _entities.ImageFiles.Where(e => e.PostID == post.PostID);
model = new Tuple<IEnumerable<Post>, IEnumerable<ImageFile>>(posts, images);
}
else
{
model = new Tuple<IEnumerable<Post>, IEnumerable<ImageFile>>(posts, null);
}
}
}
return model;
}
First, you load all posts:
var AllPosts = _entities.Posts.ToList();
Then you iterate on each post on AllPosts, and look for all posts that have the same ID than this one:
foreach (var post in AllPosts)
{
var posts = AllPosts.Where(e => e.PostID == post.PostID);
Assuming that PostID is a "unique" KEY (like you showed in the code), you will find just the post itself.
Than you create the "model". And do this for each post. At the end, "model" will contain only the last post from the first foreach.
I think you need to return a "list" of posts (and associated images), so, I would try to change the code in this way:
public List<Tuple<Post, IEnumerable<ImageFile>>> getAllBlogData()
{
var list = new List<Tuple<Post, IEnumerable<ImageFile>>();
Tuple<Post, IEnumerable<ImageFile>> tuple = null;
var AllPosts = _entities.Posts.ToList();
foreach (var post in AllPosts)
{
//checks if a blog post has images
if (post.hasImages)
{
var images = _entities.ImageFiles.Where(e => e.PostID == post.PostID);
tuple = new Tuple<Post, IEnumerable<ImageFile>>(post, images);
}
else
{
tuple = new Tuple<Post, IEnumerable<ImageFile>>(post, null);
}
list.Add(tuple);
}
return list;
}
And, of course, adapt you file to deal with the list instead of the "model".
The code above should return all the posts in your blog, through a list that has tuples where on data is the POST, and the other is the list of images associated with that post.
Related
So I am currently studying and analyzing the use of ViewModels.
In my Application (a so called "Restaurant") I want the ability for my "users" to create a menu.
When they want to create a menu: They can choose the name + the amount of persons that can join the menu. BUT also, they can add an amount of dishes that are already in the restaurant. This will be in the style of checkboxes and an 'Create'-Button at the end.
This means I had to use a ViewModel. I am currently trying to give the possibility to add a list of dishes to a menu for the creation. But I'm stuck at the for loop, used to loop through the dishes. Or better, I'm stuck at the whole concept:
What is the best way to display all the already created dishes to the CreateMenu View? Is it still possible to loop through a ViewBag if I will add them in a ViewBag?
Lets say I successfully tried to do what I wanted to do. How would I create a new Menu based (or extracted?) from the ViewModel?
In my Code, please note that the Menu - Model cannot be changed really because I already use a list of Dishes from it (In another view, where I display all the menu's and their dishes).
also ignore the possibility of wrong names or spelling mistakes in data, since I translated everything from Flemish
Models
public class Menu
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Range(0,10)]
public int AmountPersons { get; set; }
[Range(0,double.MaxValue)]
public double Price { get; set; }
public virtual List<Dish> Dishes { get; set; }
}
public class Dish
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public enum Types { VOORGERECHT, HOOFDGERECHT, DRANK, DESSERT}
public Types Type { get; set; }
public double Price { get; set; }
public virtual List<Menu> Menus { get; set; }
public virtual List<Table> Tables { get; set; }
//Checked used for the 'checkbox' in the CreateMenu-View
[NotMapped]
public bool Checked { get; set; }
}
public class MenuViewModel
{
public Menu Menu { get; set; }
public List<Dish> AddedDishes { get; set; }
}
Controller
public ActionResult CreateMenu( )
{
MenuViewModel gm = new MenuViewModel();
// Assign ALL already created dishes to the list that the user can choose.
// AddedDishes is wrong? ViewBag preferred?
gm.AddedDishes = db.Dishes.ToList();
return View(gm);
}
// Add the Menu to all the Menu's in the Database.
[HttpPost]
public ActionResult MenuAanmaken(MenuModel gm)
{
// code to save the menu with all the added dishes to the database
// Conflict!? Cannot convert the MenuViewModel to the Menu-model How do we need to extract the Menu and the AddedDishes list
// to a menu and save that one to the database?
db.Menus.Add(gm);
return View(gm);
}
View
#using VBExamen.Models
#model MenuViewModel
....
#Html.LabelFor(m => m.Menu.Name)
#Html.EditorFor(m => m.Menu.Name)
#Html.LabelFor(m => m.Menu.AmountPersons)
#Html.EditorFor(m => m.Menu.AmountPersons)
#for(int i = 0; i < Model.AddedDishes.Count; i++)
{
<tr>
<td>
#Html.DisplayFor( .Name)
#Html.HiddenFor(item => .Id)
#Html.CheckBoxFor(item => .Checked)
</td>
</tr>
}
E D I T E D _ U P D A T E (SEE BELOW)
Okay So I think I'm close now,
I edited my classes as the following:
public class MenuViewModel<T>
{
public Menu Menu { get; set; }
public List<T> DishList { get; set; }
public MenuViewModel()
{
this.Lijst = new List<T>();
}
}
Controller
public ActionResult CreateMenu(MenuViewModel<Dish> model )
{
model.DishList = db.Gerechten.ToList();
return View(model);
}
[HttpPost]
public ActionResult CreateMenu(MenuViewModel<Dish> model,List<Gerecht> SelectedList)
{
Menu t = new Menu();
t.Naam = gm.Menu.Naam;
t.AmountPersons = gm.Menu.AmountPersons;
t.Dishes = SelectedList;
db.Menus.Add(t);
return View("Menus", model);
}
View function creating list
#for (int i = 0; i < Model.DishList.Count(); i++)
{
<tr>
<td>
#Html.Label(Model.DishList[i].Naam)
<input type="hidden" name=#String.Format("DishList[{0}].Id", i) value=#Model.DishList.ElementAt(i).Id />
<input type="hidden" name=#String.Format("DishList[{0}].Name", i) value=#Model.DishList.ElementAt(i).Name />
<input type="checkbox" name=#String.Format("DishList[{0}].value", i) />
<input type="hidden" name=#String.Format("DishList[{0}].value", i) value="false" />
</td>
<br />
</tr>
}
I did this after watching about 10 tutorials about ViewModels, is my next approach better than the first one?
I think so because i get the following on my screen:
I was thinking what the next approach would be. I was thinking about comparing the 2 lists (1 of the viewmodel, 1 passed) and see the checkbox statuses?
UPDATE
After Stephen Muecke's answer I re-edited my code but found a problem that I can't seem to understand.
The answer says I should be in the position of a 1-to-many table in the form as a class:
// You have not indicated the 1-many table the dishes are saved to so adjust as required
MenuDish dish = new MenuDish()
{
MenuId = menu.ID,
DishId = dish
};
db.MenuDishes.Add(dish);
However, what we've learned at school was, that if you create lists in the data-models of the entities, linked tables will be automatically generated in the Database. And that is exactly what my DB has done (without the creation of the MenuDish class):
MenuGerechts stands for MenuDish.
This is the automatically created table done by the entity framework.
That brings me to the following questions. I have re-edited the controller to the following:
[HttpPost]
public ActionResult MenuAanmaken(MenuVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
IEnumerable<int> selectedDishes = model.Dishes.Where(x => x.IsSelected).Select(x => x.ID);
Menu menu = new Menu()
{
Naam = model.Name,
AantalPersonen = model.AmountPersons
};
foreach (int id in selectedDishes)
{
Dish g = db.Dishes.Find(id);
menu.Dishes.Add(g);
};
db.Menus.Add(menu);
db.SaveChanges();
return RedirectToAction("Menus", "Menu");
}
I get the Object reference not set to an instance of an object error and I'm understanding why ofcourse.
I have done the changes since the Data-Model Menu, already has a List of Dishes. But assuming the answer of S. Muecke, this isn't the correct way to solve this ViewModel since he proposes the use of a New Class (that is created to support the one-to-many relationship)?
This brings me to the conclusion of the following questions:
Why is it impossible or not-recommended to directly add the selected dishes to the menu instance?
Is it always needed to create the in between table 'MenuDish' in a Data-model?
Will the following code still work (showing the menu's and their dishes) after creating new Menu's?:
Controller:
public ActionResult Menus()
{
List<Menu> menus = db.Menus.ToList();
return View(menus);
}
View:
#model IEnumerable<VBExamen.Models.Menu>
#{
ViewBag.Title = "Menus";
}
<h2>Menus</h2>
<p>
#Html.ActionLink("Create New Menu", "CreateMenu")
</p>
#foreach (var item in Model)
{
<table>
<ul>
<p>#Html.DisplayFor(modelItem => item.Name)</p>
#foreach (var g in item.Dishes)
{
<li>
#Html.DisplayFor(modelItem => g.Name)
</li>
}
</ul>
</table>
}
Which outputs the following:
What would be good motivations to do this?
UPDATE 2
So I have included the following in my project:
** I have used the Table()- annotation to make it use the one that's already created**
**Model: **
[Table("MenuGerechts")]
public class MenuGerechts
{
[Key]
[ForeignKey("Menu")]
public virtual int? MenuId { get; set; }
public virtual Menu Menu { get; set; }
[ForeignKey("Dish")]
public virtual int? DishId { get; set; }
public virtual Dish Dish { get; set; }
}
I have then actually created new menus successfully! But when I go to the overview menu page (from the pic above), it only shows the Name of the menu, and not the list of meals that it includes.
The Database however didn't allow my MenuDish link table to be used for my newly created class (it created a new one, and renamed the old one with the 'old' menus with a '1' behind it:
Hence why I was asking my previous questions. Does this mean my whole approach to this exercise was wrong?
New Question:
My menuCreate ViewModel only works if i Select 1 dish? Why is this so? I get the following error The role 'MenuGerechts_Menu_Source' of the relationship 'VBExamen.Models.MenuGerechts_Menu' has multiplicity 1 or 0..1.
Firstly a view model should not contain properties which are data models. It should contains only properties which you display/edit in the view, and I recommend you read What is ViewModel in MVC?.
Based in the image of the form you have shown, your view models needs to be (display and validation attributes omitted for simplicity)
public class MenuVM
{
public int? ID { get; set; } // included so this can be used for editing as well as creating
public string Name { get; set; }
public int AmountPersons { get; set; }
public List<DishVM> Dishes { get; set; }
}
public class DishVM
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
and the controller GET method
public ActionResult CreateMenu( )
{
// Get all the dishes from the database
var dishes = db.Dishes; // modify to suit
// Initialize the view model
var model = new MenuVM()
{
Dishes = dishes.Select(x => new DishVM()
{
ID = x.Id,
Name = x.Name
}).ToList()
};
return View(model);
}
Then in the view (LabelFor() and ValidationFor() methods omitted for simplicity)
#model MenuVM
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.Name)
#Html.TextBoxFor(m => m.AmountPersons )
for(int i = 0; i < Model.Dishes.Count; i++)
{
<div>
#Html.HiddenFor(m => m.Dishes[i].ID)
#Html.HiddenFor(m => m.Dishes[i].Name)
#Html.CheckBoxFor(m => m.Dishes[i].IsSelected)
#Html.LabelFor(m => m.Dishes[i].IsSelected, Model.Dishes[i].Name)
</div>
}
<input type="submit" value="Create" />
}
And finally the POST method will be
public ActionResult CreateMenu(MenuVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Initialize and save the Menu
Menu menu = new Menu()
{
Name = model.Name,
AmountPersons = model.AmountPersons
};
db.Menus.Add(menu);
db.SaveChanges(); // you now have the ID of the new menu
// Save the dishes associated with the menu
IEnumerable<int> selectedDishes = model.Dishes.Where(x => x.IsSelected).Select(x => x.ID);
foreach(int id in selectedDishes)
{
// You have not indicated the 1-many table the dishes are saved to so adjust as required
MenuDish dish = new MenuDish()
{
MenuId = menu.ID,
DishId = dish
};
db.MenuDishes.Add(dish);
}
db.SaveChanges(); // save the selected dishes
return RedirectToAction(...); // redirect somewhere
}
Side note: Remove the [NotMapped] public bool Checked { get; set; } property from your data model.
This is only the answer to your first question... I gotta get back to work :)
I strongly advise you to use Entity Framework for storing this information, as creating the data context, Initializer(Entity Framework Requirements) and View Model will allow you to scaffold everything in your project including controllers and views. This means you take the information from the ViewModel class rather than from the view bag.
Scaffolding means that Visual Studio will create all your code to CRUD(Create, Read, Update, Delete) Controllers and Views to allow this to happen. Freeing you from either 45 mins of furious typing or hours of banging your head against a wall.
So lets do this, First we create our own context class inheriting from DbContext (A part of Entity Framework):
public class MenuContext : DbContext {
public MenuContext() : base("MenuContext") {
}
The base referenced here specifies the name in your web.config file of your connection string which we will set up momentarily. Alternatively you can specify your connection string in place of the name.
public DbSet<MenuViewModel> Menus { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
The Initializer class we will set up next populates the database if the database does not already exist at the connection string location.
class MenuInitializer : CreateDatabaseIfNotExists<MenuContext> {
protected override void Seed(MenuContext context) {
// This sets up your database to populate with whatever you type into this method.
}
}
Now you are able to go to your solution explorer, right click on your controllers folder and click add - Controller. Then specify that you want MVC 5 Controller with views, using Entity Framework. Click - Add.
A dialog will show, specify your view model, the context class we set up, make sure "Generate Views" is selected, name your controller and BAM! Build your project and view your auto created everything!
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.
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; }
}
Am new to MVC and am trying below scenario but am struck how to proceed.
In my webpage i have several section and each section has comments below it. and for fetching the comments i have written a function inside the controller as below
public ActionResult ShowPreviousComments()
{
Comments com = new Comments();
LstComments savedComments = new LstComments();
///Entity code here
foreach (var item in comments)
{
com.comments = item.Comments;
com.dTime = item.Time;
savedComments.lstCommet.Add(com);
}
return View();
}
and model data below so i can get the list in the view
public class Comments
{
public string comments { get; set; }
public DateTime? dTime { get; set; }
public int airPortId { get; set; }
}
public class LstComments
{
public List<Comments> lstCommet { get; set; }
}
My doubt is how i can hit the controller during the pageload for each sections ?
Sorry if it sounds silly or some error. Please post if i can do it in a better way
Thanks
controller
public ActionResult ShowPreviousComments()
{
Comments com = new Comments();
LstComments savedComments = new LstComments();
///Entity code here
foreach (var item in comments)
{
com.comments = item.Comments;
com.dTime = item.Time;
savedComments.lstCommet.Add(com);
}
return PartialView(savedComments);
}
View
#Html.Action("ShowPreviousComments", "SomeController")
From My Perspective
controller
public ActionResult ShowPreviousComments()
{
// Entity code here
// var comments = entity.comments;
return PartialView(comments);
}
Index.cshtml (Main view that contains own contents and comments)
// some main view content
// main view comments...
#Html.Action("ShowPreviousComments", "SomeController")
ShowPreviousComments.chtml (partial view that hold the previous comments)
#model IEnumerable<comments>
<div class="comments_container>
foreach(var item in Model)
{
<div class="comment_body">#item.Comments</div>
<div class="comment_time">#item.Time</div>
}
</div>
Maybe U can try a script to call the controller on load of the sections of the view
I have to questions:
1) How to translate this select to LINQ?
Select i.ID, i.Impression,
(Select COUNT(ImpressionsId)
from DiaryImpressions
where DiaryPostsId = '2' AND ImpressionsId = i.ID) as Num from Impressions i
2) I have to run this query inside a loop where I could replace the hard coded value 2 for the real post ID. But how could I do this in C# with MVC3? Could someone give me any example on how it works inside foreach loop and how could I display this value in the view?
Thanx a lot =)
Have you already got a database context/repository set up to communicate with the database in your MVC project? Below I'm assuming you've got a database context named dbContext.
Note: If you're not using Razor, replace the "#" with "<%" and close with "%>"
You can either put the item in the ViewBag, or in the Model (I prefer Model) like below. I'm not sure what you want to do with these counts, so i've put them in a list. You'll need a class to put your impression info in, i've added it below;
Model Code:
public class MyModel
{
public class ImpressionInfo //just used to store your results sub class of the model
{
public ImpressionInfo(id, impression, diaryImpressionCount)
{
Id = id;
Impression = impression;
DiaryImpressionCount = diaryImpressionCount
}
public int Id { get; set; }
public int Impression { get; set; } //is this an int? you didn't say
public int DiaryImpressionCount { get; set; }
}
public MyModel()
{
var impressionInfo = new List<ImpressionInfo>()
foreach (var di in dbContext.DiaryImpressions)
{
ImpressionInfos.Add(new ImpressionInfo(
di.Id,
di.Impression,
dbContext.DiaryPosts
.Count(dp => dp.ImpressionsId == di.ID));
}
}
public List<ImpressionInfo> ImpressionInfos { get; set; }
then in the view
View Code:
#model MyModel
#if(Model.ImpressionInfos.Count > 0)
{
<table>
<tr>
<td>Impression</td>
<td>Count</td>
</tr>
foreach (var i in Model.ImpressionInfos)
{
<tr>
<td>#i.Impression</td>
<td>#i.DiaryImpressionCount</td>
</tr>
}
}
else
{
<p>No Impression infos</p>
}
Hope this helps.