Lets's Say that i have a two model classes; Project, and Comment as following :
public class Project
{
Public int ProjectID { get; set; }
public string Title { get; set; }
public virtual List<Comment> Comments { get; set; }
}
public class Comment
{
Public int CommentID { get; set; }
public string Text { get; set; }
}
I used the CRUD creation feature when i created the "controller and the views" for my Project class.
Now, in the 'Details' view for the Project, i want to add form to add comments to this project, i want the 'Details' view to be something like :
Project Name : -- Project Name Goes Here --
Comments : 1. ---------
2. ---------
[Text Area To Enter Comment] and [SUBMIT] button
The submit button should add comment to the project's comments list.
How do I achieve this?
I recommend creating a ViewModel that represents all the data you need for a view. These ViewModels are specific to MVC.
Model:
public class IndexViewModel
{
public Project Project { get; set; }
public IEnumerable<Comment> Comments { get; set; }
public Comment NewComment { get; set; }
}
Controller Method:
public ActionResult Index()
{
var model = new IndexViewModel();
// populate data, including an empty NewComment
model.NewComment = new Comment();
model.NewComment.ProjectId = model.Project.ProjectId;
return View(model);
}
View:
#model IndexViewModel
#using (Html.BeginForm("Comment", "Create"))
{
#Html.EditorFor(m => m.NewComment.CommentText)
#Html.HiddenFor(m => m.NewComment.ProjectId)
}
This means adding or removing data a view needs is pretty straight forward. The form should only need to be around NewComment. The post model would look like:
Model:
public class CreateCommentViewModel
{
public Comment NewComment { get; set; }
}
Control Method:
public ActionResult Create(CreateCommentViewModel model)
{
// logic for creating comment
}
DotNetFiddle Example. The only problem with Dot Net Fiddle is it only supports a single view (that I know of) so when you pass the Text of the new comment, I throw an exception with the text of the comment.
Erik Philips was close to answer, actually his solution works, but i found most accurate answer to my question using Html.Action.
// 1. Add this line to Project's Detail View.
#Html.Action("CreateComment","Comment", new { ProjectID = Model.ProjectID })
// 2. Add this method to the Comment Controller class, and send the id
// to the comment view.
public ActionResult CreateComment(int ProjectID)
{
return View(new Comment() { ProjectID = ProjectID });
}
// 3. Create view for the CreateComment controller action
#using (Html.beginForm("SubmitComment","Comment"))
{
#Html.HiddenFor(ProjectID=>Model.ProjectID) // This value you send it
#Html.EditorFor(model=>Model.Text)
<input type="submit" value="Add Comment" />
}
// 4. add method to the comment controller
// since i alreay public ActionResult Details(int id) in the project controller
// to display the project's details and comments. i will call it after adding comment
public ActionResult SubmitComment(Comment comment)
{
dbContext = new myDatabaseContext();
dbContext.Comments.Add(comment);
dbContext.SaveChanges();
return RedirectToAction("Details","Project", new { id=comment.ProjectID })
}
Thanks for contributing in this post
Related
I want to create a form that is comprised of fields that come from separate models and then, when submitted, updates the corresponding tables in the database.
I couldn't find the answer anywhere so I tinkered with things until I made something work. Being new to all of this, however, I doubt the efficacy of my solution. I'm hoping that by stating my problem and showing my solution you can better understand what I need and be able to help me find the best solution for my problem.
I like simple, abstract examples so here is what I currently have working:
First, I make a Color Model...
namespace example.Models
{
public class Color
{
public int ColorID { get; set; }
public string Name { get; set; }
}
}
Then I make a Shape Model...
namespace example.Models
{
public class Shape
{
public int ShapeID { get; set; }
public string Name { get; set; }
}
}
Next, I make a simple ViewModel...
namespace example.ViewModels
{
public class ColorAndShapeViewModel
{
public Color Color { get; set; }
public Shape Shape { get; set; }
}
}
Here is my DbContext...
namespace example.DAL
{
public class MyContext : DbContext
{
public DbSet<Color> Colors { get; set; }
public DbSet<Shape> Shapes { get; set; }
}
}
Next, the Controller...
namespace example.Controllers
{
public class MyController : Controller
{
private MyContext db = new MyContext();
public ActionResult Index()
{
return View();
}
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ColorAndShapeViewModel viewmodel)
{
Color newColor = new Color();
newColor.Name = viewmodel.Color.Name;
Shape newShape = new Shape();
newShape.Name = viewmodel.Shape.Name;
db.Colors.Add(newColor);
db.Shapes.Add(newShape);
db.SaveChanges();
return RedirectToAction("Index");
}
}
}
Lastly, the View...
#model example.ViewModels.ColorAndShapeViewModel
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.LabelFor(model => model.Color.Name)<br />
#Html.TextBoxFor(model => model.Color.Name)<br />
<br />
#Html.LabelFor(model => model.Shape.Name)<br />
#Html.TextBoxFor(model => model.Shape.Name)<br />
<br />
<input type="submit" value="Create" />
}
Thus far, everything works as expected: I am able to enter data for separate models into a single form and submit it. The database is updated (the color goes into the color table, the shape into the shape table) and all is well—or is it? In reality, I will have a much longer form using several models which are related to each other. My present solution would require a large controller, full of newObject.Property = viewModel.Object.Property statements. Isn't that undesirable? Is there a better way to handle this?
Thanks!
Conclusion in Images, can be found in the bottom
I'm having some trouble to get how Forms work in MVC (as I'm a WebForms Developer and really wanna start using MVC in one big project)
I took the MVC2 Web Project and add a simple ViewModel to it
namespace TestForms.Models
{
public class myFormViewModel
{
public myFormViewModel() { this.Options = new List<myList>(); }
public string Name { get; set; }
public string Email { get; set; }
public List<myList> Options { get; set; }
}
public class myList
{
public myList() { this.Value = this.Name = ""; this.Required = false; }
public string Name { get; set; }
public string Value { get; set; }
public string Type { get; set; }
public bool Required { get; set; }
}
}
Created a Strongly Typed View, passed a new object to the view and run it.
When I press submit, it does not return what's in the Options part... how can I bind that as well?
my view
alt text http://www.balexandre.com/temp/2010-10-11_1357.png
filling up the generated form
alt text http://www.balexandre.com/temp/2010-10-11_1353.png
when I press Submit the Options part is not passed to the Model! What am I forgetting?
alt text http://www.balexandre.com/temp/2010-10-11_1352.png
Conclusion
Changing the View loop to allocate the sequential number, we now have
<%= Html.TextBox("model.Options[" + i + "].Value", option.Value)%>
model is the name of our Model variable that we pass to the View
Options is the property name that is of type List
and then we use the property name
Looking at your UI it seems that you did not put the data from the Options member on it.
<% foreach (myList obj in Model.Options) { %>
// Add the object to your UI. they will be serialized when the form is submitted
<% } %>
Also check that you enclose the data in a form element
EDIT:
Sorry! I did'nt realized that you was filling the object inside the controller. Can you please show the code you have in the view?
I have found myself with at little problem, and I think a custom model binder is the way to go.
My Domain model looks like this,readly standard
I got a Page and a Template. The Page has the Template as a ref.
So the Default asp.net mvc Binder, does not know how to bind it, therefore I need to make some rules for it. (Custom Model Binder)
public class PageTemplate
{
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual DateTime? Created { get; set; }
public virtual DateTime? Modified { get; set; }
}
public class Page
{
public virtual string Title { get; set; }
public virtual PageTemplate Template { get; set; }
public virtual string Content { get; set; }
public virtual DateTime? Created { get; set; }
public virtual DateTime? Modified { get; set; }
}
So I have Registreted the ModelBinder in globals.asax
ModelBinders.Binders.Add(typeof(Cms.Domain.Entities.Page),
new Cms.UI.Web.App.ModelBinders.PageModelBinder(
new Services.GenericApplicationService<Cms.Domain.Entities.Page>().GetEntityStore()
)
);
My ModelBinder tage a paremeter, witch is my Repository, where I get all my Entities ( Page, Template )
My Controller for a Page looks like this.
I have posted into a Create Controler, it does not matter for now, if it was a Update method.
Since I in this case have a dropdown, that represents the Template, I will get an ID in my form collection.
I then call: TryUpdateModel and I got a hit in my PageModelBinder.
[AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Create(FormCollection form)
{
Page o = new Page();
string[] exclude = new { "Id" }
if (base.TryUpdateModel<Page>(o, string.Empty, null, exclude, form.ToValueProvider()))
{
if (ModelState.IsValid)
{
this.PageService.Add(o);
this.CmsViewData.PageList = this.PageService.List();
this.CmsViewData.Messages.AddMessage("Page is updated.", MessageTypes.Succes);
return View("List", this.CmsViewData);
}
}
return View("New", this.CmsViewData);
}
So I end op with the Model Binder.
I have search the internet dry for information, but im stock.
I need to get the ID from the FormCollection, and parse it to at Model from my IEntityStore.
But how ?
public class PageModelBinder : IModelBinder
{
public readonly IEntityStore RepositoryResolver;
public PageModelBinder(IEntityStore repositoryResolver)
{
this.RepositoryResolver = repositoryResolver;
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
if (modelType == typeof(Cms.Domain.Entities.Page))
{
// Do some magic
// Get the Id from Property and bind it to model, how ??
}
}
}
// Dennis
I hope, my problom is clear.
Did find a work around.
I download the sourcecode for asp.net r2 rtm 2
And did copy all code for the default ModelBinder, and code it need. Did some minor change, small hacks.
the work around is doing a little hack in this method:
[SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
Justification = "The target object should make the correct culture determination, not this method.")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "We're recording this exception so that we can act on it later.")]
private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType)
{
try
{
object convertedValue = valueProviderResult.ConvertTo(destinationType);
return convertedValue;
}
catch (Exception ex)
{
try
{
// HACK if the binder still fails, try get the entity in db.
Services.GenericApplicationService<Cms.Domain.Entities.PageTemplate> repo;
repo = new Services.GenericApplicationService<Cms.Domain.Entities.PageTemplate>();
int id = Convert.ToInt32(valueProviderResult.AttemptedValue);
object convertedValue = repo.Retrieve(id);
return convertedValue;
}
catch (Exception ex1)
{
modelState.AddModelError(modelStateKey, ex1);
return null;
}
}
}
This question is closed.
I have searched like a fool but does not get much smarter for it..
In my project I use Entity Framework 4 and own PoCo classes and I want to use DataAnnotations for validation. No problem there, is how much any time on the Internet about how I do it. However, I feel that it´s best to have my validation in ViewModels instead and not let my views use my POCO classes to display data.
How should I do this smoothly? Since my repositories returns obejekt from my POCO classes I tried to use AutoMapper to get everything to work but when I try to update or change anything in the ModelState.IsValid is false all the time..
My English is really bad, try to show how I am doing today instead:
My POCO
public partial User {
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
And my ViewModel
public class UserViewModel {
public int Id { get; set; }
[Required(ErrorMessage = "Required")]
public string UserName { get; set; }
[Required(ErrorMessage = "Required")]
public string Password { get; set; }
}
Controller:
public ActionResult Edit(int id) {
User user = _userRepository.GetUser(id);
UserViewModel mappedUser = Mapper.Map<User, UserViewModel>(user);
AstronomiGuidenModelItem<UserViewModel> result = new AstronomiGuidenModelItem<UserViewModel> {
Item = mappedUser
};
return View(result);
}
[HttpPost]
public ActionResult Edit(UserViewModel viewModel) {
User user = _userRepository.GetUser(viewModel.Id);
Mapper.Map<UserViewModel, User>(viewModel, user);
if (ModelState.IsValid) {
_userRepository.EditUser(user);
return Redirect("/");
}
AstronomiGuidenModelItem<UserViewModel> result = new AstronomiGuidenModelItem<UserViewModel> {
Item = viewModel
};
return View(result);
}
I've noticed now that my validation is working fine but my values are null when I try send and update the database. I have one main ViewModel that looks like this:
public class AstronomiGuidenModelItem<T> : AstronomiGuidenModel {
public T Item { get; set; }
}
Why r my "UserViewModel viewModel" null then i try to edit?
If the validation is working, then UserViewModel viewModel shouldn't be null... or is it that the client side validation is working but server side isn't?
If that's the case it could be because of the HTML generated.
For instance, if in your view you have:
<%: Html.TextBoxFor(x => x.Item.UserName) %>
The html that gets rendered could possibly be:
<input name="Item.UserName" id="Item_UserName" />
When this gets to binding on the server, it'll need your action parameter to be named the same as the input's prefix (Item). E.g.
public ActionResult Edit(UserViewModel item) {
To get around this, do as above and change your action parameter to item OR you could encapsulate the form into a separate PartialView which takes the UserViewModel as it's model - that way the Html.TextBoxFor won't be rendered with a prefix.
HTHs,
Charles
Ps. If I'm totally off track, could you please post some code for the view.
I have a parent object book, and a property of that object is publisher. Everytime I ad a book, it is adding a new publisher, even if the publisher already exists. Can someone tell me how to add the book and instead of adding the publisher again, just reference an existing one? The code i am using is below... Thanks in advance!
public class Book
{
public int BookID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreateDate { get; set; }
public virtual Publisher Publisher { get; set; }
}
public class Publisher
{
public int PublisherID { get; set; }
public string Address { get; set; }
}
public class SqlCEDataStore : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Publishers> Publishers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.IncludeMetadataInDatabase = false;
}
}
public class TimeSinkRepository : IRepository<Book>
{
private static SqlCEDataStore context = new SqlCEDataStore();
public int Add(Book entity)
{
context.Books.Add(entity);
return context.SaveChanges();
}
}
var book = new Book()
{
Title = "New Title",
Description = "New Description",
CreateDate = DateTime.Now,
Publisher = new Publisher() { PublisherID = 1 }
};
var repository = new BookRepository();
var result = repository.Add(book);
The problem is in the line:
Publisher = new Publisher() { PublisherID = 1 }
Object context doesn't know that this is existing publisher. It is newly created entity so Object context will perform insert operation. You have to say object context that the publisher object is not newly created. One way to do that is modification of your Add method:
public int Add(Book entity)
{
context.Books.Add(entity);
// 0 means new one, other values mean existing one
if (entity.Publisher.PublisherID > 0)
{
context.ObjectStateManager.ChangeObjectState(entity.Publisher, EntityState.Unchanged);
}
context.SaveChanges();
}
It you can solve this by making sure the Publisher is attached to Publishers context before adding the Book entity (this way it knows it's a Publisher from the dbcontext and not a new one that it needs to add (again))
context.Publishers.Attach(book.Publisher); // This is only possible if the Publisher is not new
context.Books.Add(book);
the problem is in this line
Publisher = new Publisher() { PublisherID = 1 }
You should do a fetch method so something like this
- Get the Publisher you want from the context (eg where id = 1)
- Set the returned object as the publisher for your new book object
- The context should sort the rest out for you. when you save the book. (no need to mess with the object state manager)
Good luck, if you cant get this working put up some code of it and i will help you though it.