In an Edit action in ASP.NET MVC, certain fields can be hidden from user with HiddenFieldFor. However this doesn't protect the fields (such as ID, data creation date) from being edited.
For example, a model Student has fields Id, Name and Birthday. I like to allow users to update the Name, but not Id nor Birthday.
For an Edit action like this
public ActionResult Edit(Student student)
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(student);
}
How can I prevent Id and Birthday from being edited? Thanks!
You should use a view model which contains only the properties that you want to be edited:
public class EditStudentViewModel
{
public string Name { get; set; }
}
and then:
public ActionResult Edit(StudentViewModel student)
{
...
}
Another technique which I don't recommend is to exclude certain properties from binding:
public ActionResult Edit([Bind(Exclude = "Id,Birthday")]Student student)
{
...
}
or include:
public ActionResult Edit([Bind(Include = "Name")]Student student)
{
...
}
I assume you have to have the properties in your Model so that in View you can use them to render useful information e.g. an ActionLink with ID or some readonly Text.
In this case you can define your model with an explicit binding:
[Bind(Include = "Name")]
public class Student
{
int Id { get; set; }
int Name { get; set; }
DateTime Birthday { get; set; }
}
This way when updating your model, if the user submits an extra Id it will not be bound.
Another idea I like is having your model know its bindings per scenario and have them compiler validated:
public class ModelExpression<T>
{
public string GetExpressionText<TResult>(Expression<Func<T, TResult>> expression)
{
return ExpressionHelper.GetExpressionText(expression);
}
}
public class Student
{
public static string[] EditBinding = GetEditBinding().ToArray();
int Id { get; set; }
int Name { get; set; }
DateTime Birthday { get; set; }
static IEnumerable<string> GetEditBinding()
{
ModelExpression<Student> modelExpression = new ModelExpression<Student>();
yield return modelExpression.GetExpressionText(s => s.Name);
}
}
This way in your Action when calling TryUpdateModel you can pass this information.
Related
I was practicing User.Identity and timestamps functions in ASP.NET MVC 5,
So I created a student class filled some properties, I just wanted to test if it is capturing timestamps and userId, so user id is getting captured and datetime too, problem is whenever I'm editing a record and save it, its created date becomes Null and modified date is updated, please review the code and help.
Thanks in advance.
Below is the Code
{
public class BaseEntity
{
public DateTime? DateCreated { get; set; }
public string UserCreated { get; set; }
public DateTime? DateModified { get; set; }
public string UserModified { get; set; }
}
public class Student : BaseEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Subject { get; set; }
public string Class { get; set; }
public Section Section { get; set; }
public byte SectionId { get; set; }
}
then I used Codefirst approach and created an application Database and added this code in Identity Model
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<Student> Students { get; set; }
public override int SaveChanges()
{
AddTimestamps();
return base.SaveChanges();
}
//public override async Task<int> SaveChangesAsync()
//{
// AddTimestamps();
// return await base.SaveChangesAsync();
//}
private void AddTimestamps()
{
var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
var currentUsername = !string.IsNullOrEmpty(System.Web.HttpContext.Current?.User?.Identity?.Name)
? HttpContext.Current.User.Identity.Name
: "Anonymous";
foreach (var entity in entities)
{
if (entity.State == EntityState.Added)
{
((BaseEntity)entity.Entity).DateCreated = DateTime.UtcNow;
((BaseEntity)entity.Entity).UserCreated = currentUsername;
}
else
((BaseEntity)entity.Entity).DateModified = DateTime.UtcNow;
((BaseEntity)entity.Entity).UserModified = currentUsername;
}
}
public DbSet<Section> Sections { get; set; }
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
I have created a simple controller with create,edit and dispay actions.
The code you posted doesn't show DateCreated being set to null as far as I can see. I think the issue is when you save an existing record you do not have the DateCreated or UserCreated fields in your view. So when you post the form the MVC model binder doesn't see them and thus sets them to null (I'm assuming your are binding to the Student model in your controller action).
In your edit view add the following hidden fields:
#Html.HiddenFor(model => model.DateCreated)
#Html.HiddenFor(model => model.UserCreated)
Now when you post the form the MVC model binder will bind these values to your model and save them to the database.
public class Class1
{
public Guid Class1ID { get; set; }
public string class1string { get; set; }
public virtual Class2 Class2 { get; set; }
}
public class Class2
{
public Guid Class2ID { get; set; }
public string class2string { get; set; }
}
// POST: Class1/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("Class1ID,Class2,class1string")] Class1 class1)
{
if (id != class1.Class1ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(class1);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!Class1Exists(class1.Class1ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(class1);
}
Instead of the Edit changing the data that is in the child table, it creates an new row in the table and changes the GUID in the parent table. The Parent table is edited correctly.
Any help will be greatly appreciated.
In your Class 1 also add the foreignkey id that should match the primary key property. EF will know its related
public class Class1
{
public Guid Class1ID { get; set; }
public string class1string { get; set; }
public Guid? Class2ID { get; set; }
[ForeignKey("Class2ID ")]//probably not needed as names match
public virtual Class2 Class2 { get; set; }
}
public class Class2
{
public Guid Class2ID { get; set; }
public string class2string { get; set; }
}
that way in your update class1 you just need to check you pass the correct Class2ID property and not worry about the navigation object property Class2.
For saving you need to spesify it was modified
public async Task<IActionResult> Edit(Class1 class1)
{
...
_context.Entry(class1).State = EntityState.Modified;
await _context.SaveChangesAsync();
I have also been struggling with this problem.
When I make an edit and save it I hit the function public async Task Edit(Guid id,[Bind("Class1ID,Class2,class1string")] Class1 class1) (as you would expect) -
All the values are correct except class1.Class2.Class2ID which is an empty guid {00000000-0000-0000-0000-000000000000}
As a result when SaveChangesAsync is called EF creates a new record for Class2, rather than updating the existing record as intended.
The is because the binding is failing, it can't find the value for Class2.Class2ID.
This is because the Get is failing to load this value as it is almost certainly not on the page.
Add the following line to your view markup (suggest next to input type="hidden" asp-for="Class1ID")
<input type="hidden" asp-for="Class2.Class2ID" />
This should enable the binding to work.
I hope this helps.
I have a two relational Model first one is
Teacher.cs
public class Teachers
{
[Key]
public int TeacherID { get; set; }
public string TeacherName { get; set; }
public string TeacherLname { get; set; }
public int DepartmentID { get; set; }
public string Image { get; set; }
public Department Department { get; set; }
}
and second is Department.cs
public int DepartmentID { get; set; }
public string DepartmentName { get; set; }
public string Image { get; set; }
public List<Teachers> Teachers { get; set; }
When I'm creating a new record, I' choose a Department Name for teacher, and It's adding fine. But When I want to Delete a record there is a error like this
The ViewData item that has the key 'DepartmentID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
Line 32: #Html.DropDownList("DepartmentID", String.Empty)
I don't understand what I need to do. Can you help me?
Thanks a lot
TeacherController
EDIT :
//
// GET: /Teachers/Delete/5
[Authorize(Roles = "A")]
public ActionResult Delete(int id = 0)
{
Teachers teachers = db.Teachers.Find(id);
if (teachers == null)
{
return HttpNotFound();
}
return View(teachers);
}
//
// POST: /Teachers/Delete/5
[Authorize(Roles = "A")]
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Teachers teachers = db.Teachers.Find(id);
db.Teachers.Remove(teachers);
db.SaveChanges();
return RedirectToAction("Index");
}
When you pass an empty string into Html.DropDownList() it looks for a list of items to populate the dropdownlist from the first parameter in the ViewData collection. However, there is already an item in that collection that is of type Int32.
This is one of the many confusing scenarios that happen when you use Html.DropDownList() rather than using a strongly typed model and Html.DropDownListFor()
I suggest you do this:
#Html.DropDownListFor(x => x.DepartmentID, Model.Departments)
You will need to populate your model with a Departments object that is a list of Departments
This question is in reference to the project discussed here. After resolving the previous problem I have run into a new one. When The Student object is saved, the list of courses associated with it is not saved. I can see the collection of course objects when I mouse over the student object after setting a breakpoint:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddCourseVM (AddCourseViewModel vModel)
{
Student stu = db.Students.Find(vModel.Student.ID);
foreach (Course c in vModel.PossibleCourses)
{
if (c.Selected)
{
BaseCourse bc = db.BaseCourses.Find(c.BaseCourse.ID);
c.BaseCourse = bc;
c.Student = stu;
stu.CoursesTaken.Add(c);
}
}
if (stu != null)
{
db.Entry(stu).State = EntityState.Modified; //breakpoint here
db.SaveChanges();
}
return RedirectToAction("ListTakenCourses", stu);
}
public ActionResult ListTakenCourses (Student stu)
{
List<Course> taken = stu.CoursesTaken.ToList();
foreach (Course c in taken)
{
c.BaseCourse = db.BaseCourses.Find(c.BaseCourse.ID);
}
ViewBag.CoursesTaken = taken;
return View(stu);
}
But when I pass the object to the next method, the list of courses taken comes back null. The courses are being saved to the database, I can see them when I go into the SQL Server explorer, but for some reason they are not being attached to the student object. The code for the objects:
public class Student
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string WNumber { get; set; }
public int HoursCompleted { get; set; }
public double GPA { get; set; }
public Concentration StudentConcentration { get; set; }
public virtual ICollection<Course> CoursesTaken { get; set; }
public virtual ICollection<Course> CoursesRecommended { get; set; }
}
and:
public class Course
{
public int ID { get; set; }
public string Semester { get; set; }
public Grade? Grade { get; set; }
public bool Selected { get; set; }
public BaseCourse BaseCourse { get; set; }
public Student Student { get; set; }
}
Something that may be important, but that I don't really understand: when I look at the table for the Course object in the database, there are three columns, called Student_ID, Student_ID1, and Student_ID2. I assume they relate to the student associated with the object and the two ways it can be associated (recommended or taken), but the odd thing is that Student_ID is always null, while the other two sometimes have a value and sometimes do not. I have not even begun to implement the recommendation process, so there is no way that list is being filled.
I reworked the classes and now it seems to be working. I changed the Course object to:
public class Course
{
public int ID { get; set; }
public string Semester { get; set; }
public Grade? Grade { get; set; }
public bool Selected { get; set; }
public int BaseCourseID { get; set; }
public int StudentID { get; set; }
public BaseCourse BaseCourse { get; set; }
public Student Student { get; set; }
}
and the controller methods to:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddCourseVM (AddCourseViewModel vModel)
{
Student stu = db.Students.Find(vModel.Student.ID);
foreach (Course c in vModel.PossibleCourses)
{
if (c.Selected)
{
BaseCourse bc = db.BaseCourses.Find(c.BaseCourse.ID);
c.BaseCourse = bc;
c.Student = stu;
stu.CoursesTaken.Add(c);
db.Entry(c).State = EntityState.Added;
}
}
if (stu != null)
{
db.Entry(stu).State = EntityState.Modified;
db.SaveChanges();
}
return RedirectToAction("ListTakenCourses", stu);
}
public ActionResult ListTakenCourses (Student stu)
{
List<Course> taken = db.Courses.Where(c => c.StudentID == stu.ID).ToList();
foreach (Course c in taken)
{
c.BaseCourse = db.BaseCourses.Find(c.BaseCourseID);
c.Student = stu;
stu.CoursesTaken.Add(c);
}
ViewBag.CoursesTaken = taken;
return View(stu);
}
And it is now displaying the courses I add on the next page, but it seems odd that I have to save the child objects separately from the parent and that I have to get the list from the database manually instead of being able to use the object structure. Is this intended behavior, or is there a better way of doing what I'm trying to do (add a list of child objects (courses) to a student object, save the relationship to the database, and then display the list of added objects)?
You are not "passing the object to the next method". You are serializing the object and passing it on the URL, then deserializing it on the other end with this method:
return RedirectToAction("ListTakenCourses", stu);
This is not the way to go about things. What you should be doing is passing a single id, such as the student id. Then, in ListTakenCourses you look up the student again in the database, which if you are doing your query correctly will fully populate the objects.
return RedirectToAction("ListTakenCourses", new { id = stu.StudentID });
public ActionResult ListTakenCourses (int id)
{
List<Course> taken = db.Courses.Where(c => c.StudentID == id).ToList();
//...
}
Something very simple but I am looking for the best way to do it. I have a Movie entity, each Movie can be in one Language only(a lookup table with English, French,etc...). Now I'm trying to load all the available languages in the lookup in the Movie Create Page, the Movie View Model:
namespace Project.ViewModels {
public class Movie {
[Key]
public int ID { get; set; }
public string Title { get; set; }
public string Rating { get; set; }
public string Director { get; set; }
public string Plot { get; set; }
public string Link { get; set; }
public string Starring { get; set; }
public int DateCreated { get; set; }
public string Genre { get; set; }
[Display(Name = "Language")]
public int LanguageID { get; set; }
// Navigational Properties
public virtual MovieLanguage Language { get; set; }
}
}
The MovieLanguage View model:
namespace MAKANI.ViewModels {
public class MovieLanguage {
[Key]
public int ID { get; set; }
public string Language { get; set; }
public virtual ICollection<Movie> Movies { get; set; }
}
}
The controller action:
public ActionResult MovieCreate() {
using (MAKANI.Models.Entities db = new MAKANI.Models.Entities()) {
List<Models.MoviesLanguages> enLanguages = db.MoviesLanguages.ToList();
IEnumerable<SelectListItem> selectList =
from m in enLanguages
select new SelectListItem {
Text = m.Language,
Value = m.ID.ToString()
};
ViewBag.SelectLanguage = selectList.ToList();
return View();
}
}
And in the View page i have
<div class="editor-field">
#Html.DropDownList("Language", ViewBag.SelectLanguage);
</div>
Howver I am getting this error in the View:
'System.Web.Mvc.HtmlHelper' has no applicable method named 'DropDownList' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax
Not sure what the problem might be?
Another questions regarding this approach:
Should a create a view model for the MovieLanguage entity in the first place, knowing that it servers only as a lookup table(so it doesnt require any Create/Edit/Delete action, Only List/Read maybe), should I be depending on the EF entities directly in that case?
Have a Languages Collection Property in your Movie ViewModel and a SelectedLanguage Property to get the selected Language ID when the form submits. It is not necessary that your ViewModel should have all the properties like your domain model. Have only those properties which the View needs.
public class Movie
{
public int ID { set;get;}
public string Title { set;get;}
//Other Relevant Properties also.
public IEnumerable<SelectListItem> Languages { set;get;}
public int SelectedLanguage { set;get;}
public Movie()
{
Languages =new List<SelectListItem>();
}
}
Now in your GET Action, Create an object of your Movie ViewModel and set the Languages Collection property and send that to the View. Try to avoid using ViewBag for passing data like this. ViewBag makes our code dirty.Why not use the strongly typed ViewModels to its full extent ?
public ActionResult CreateMovie()
{
var vm=new Movie();
// TO DO : I recommend you to abstract code to get the languages from DB
// to a different method so that your action methods will be
// skinny and that method can be called in different places.
var enLanguages = db.MoviesLanguages.ToList();
vm.Languages= = from m in enLanguages
select new SelectListItem {
Text = m.Language,
Value = m.ID.ToString()
};
return View(vm);
}
And in your view which is strongly typed to our Movie ViewModel, use the DropDownListFor Hemml helper method
#model Movie
#using(Html.Beginform())
{
#Html.DropDownListFor(x => x.SelectedLanguage,
new SelectList(Model.Languages, "Value", "Text"), "Select Language")
<input type="submit" />
}
Now when you post the form, you will get the selected languageId in the SelectedLanguage Property of your ViewModel
[HttpPost]
public ActionResult CreateMovie(Movie model)
{
If(ModelState.IsValid)
{
//check model.SelectedLanguage property here.
//Save and Redirect (PRG pattern)
}
//you need to reload the languages here again because HTTP is stateless.
return View(model);
}