I got a strange issue.
When I'm trying to edit an object in mvc, just one of the object's properties is not changing, all the other properties gets their new values.
I'm using EF with repositories and unit of work.
This is the object:
public partial class License
{
public int LicenseID { get; set; }
public string Lic_LicenseRequest { get; set; } // <-- this is the problematic property
public int Lic_LicenseMaxClientsNum { get; set; }
public string Lic_LicenseComments { get; set; }
}
This is the controller:
public ActionResult Edit(int id = 0)
{
LicenseModel model = new LicenseModel();
model.License = unit.LicenseRepository.GetObjectById(id);
if (model.License == null)
{
return HttpNotFound();
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(LicenseModel model)
{
if (ModelState.IsValid)
{
unit.LicenseRepository.EditObject(model.License);
unit.Save();
return RedirectToAction("Index");
}
LicenseModel newModel = new LicenseModel();
newModel.License = unit.LicenseRepository.GetObjectById(model.License.LicenseID);
return View(newModel);
}
This is the View:
#model MySystem.UI.MVC.Models.LicenseModel
<body>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>License</legend>
#Html.HiddenFor(model => model.License.LicenseID)
<div class="form-group">
#Html.LabelFor(model => model.License.Lic_LicenseMaxClientsNum)
#Html.TextBoxFor(model => model.License.Lic_LicenseMaxClientsNum)
<p class="help-block">#Html.ValidationMessageFor(model => model.License.Lic_LicenseMaxClientsNum)</p>
</div>
<div class="form-group">
#Html.LabelFor(model => model.License.Lic_LicenseRequest)
#Html.TextBoxFor(model => model.License.Lic_LicenseRequest)
<p class="help-block">#Html.ValidationMessageFor(model => model.License.Lic_LicenseRequest)</p>
</div>
<div class="form-group">
#Html.LabelFor(model => model.License.Lic_LicenseComments)
#Html.TextAreaFor(model => model.License.Lic_LicenseComments)
<p class="help-block">#Html.ValidationMessageFor(model => model.License.Lic_LicenseComments)</p>
</div>
<p>
<button type="submit" value="Save">Save</button>
</p>
</fieldset>
}
</body>
I want to be clear that I didn't bring here all the properties for the License object beacuse it's a lot. But I use ViewModel beacuse some of the properties require dropdownlist (and I'm not usind ViewBag or ViewData).
I tried to put directly in the database a temporary string for the property, but the it doesn;t show any value at all.
I'm thinking that it might be beacuse I added this property a while after loading the db to the EF and to the VS project. I did refresh the EF of course, but it might not success.
How can I be sure that the property in the EF and project is comunating correctly with the db? What else could be the problem? (again, all the other properties are correctly modified).
Like I suspect, unfortunately the EF refresh was not successfully made, so I refresh the hole EF from the databse and it worked.
If you have used Repository and Unit Of Work patterns; one more logical step would be using EF context with a per request life time (not Session, not Application, but Request). This way you do not have to refresh EF much, which is not a good thing in a web app.
Db Context will be disposed at the end of request life time (one experience: do not call SaveChanges at that point, but at a logical point in your app; preferably in your service class).
Unity dependency injection library already has a MVC bootstrapper that will provide you control over your injected object life time.
This library implemented a generic Repository pattern alongside Unit Of Work and has a video to how put things together in a MVC app. I've used it and enjoyed it.
Related
I am using a Dictionary<string, T> data structure to store phone numbers in a view model. The user can add or remove them client side before posting them back to the server.
Back story: I am using a Dictionary, because using a List<T> data structure with ASP.NET MVC 5 requires the names of form fields to contain sequential indexes starting with zero, and it becomes a real pain for JavaScript to add or remove those fields on screen without re-sequencing the index values. I found using a dictionary made it very easy. Now I'm doing a proof of concept task to enable dependency injection that allows us to use our NHibernate session to query the database during validations, and use the same session that the controllers and view models use instead of the "singleton" pattern that FluentValidation uses with MVC 5.
When using the [Validator(typeof(T))] attribute above view models, messages appear by the fields just fine, but the validator instances are singletons in the AppDomain, and the NHibernate session used by the validators is not the same one used by the controllers. This causes data to become out of sync during data validations. Validations that check the database start returning unexpected results, because NHibernate is caching so much data on the server, and it effectively has 2 separate caches.
Project setup
ASP.NET MVC 5
.NET Framework 4.5.1 (but we could upgrade)
FluentValidation v8.5.0
FluentValidation.Mvc5 v8.5.0
FluentValidation.ValidatorAttribute v8.5.0
View models
public class PersonForm
{
public PhoneFieldsCollection Phones { get; set; }
}
public class PhoneFieldsCollection
{
public Dictionary<string, PhoneNumberFields> Items { get; set; }
}
public class PhoneNumberFields
{
[Display(Name="Country Code")]
[DataType(DataType.PhoneNumber)]
public string CountryCode { get; set; }
[Display(Name="Phone Number")]
[DataType(DataType.PhoneNumber)]
public string PhoneNumber { get; set; }
[DataType(DataType.PhoneNumber)]
public string Extension { get; set; }
[Display(Name="Type")]
public string TypeCode { get; set; }
}
View model validators
public class PersonFormValidator : AbstractValidator<PersonForm>
{
private readonly IPersonRepository repository;
public PersonFormValidator(IPersonRepository repository)
{
// Later on in proof of concept I will need to query the database
this.repository = repository;
RuleForEach(model => model.Phones)
.SetValidator(new PhoneNumberFieldsValidator());
}
}
public class PhoneNumberFieldsValidator : AbstractValidator<PhoneNumberFields>
{
public PhoneNumberFieldsValidator()
{
RuleFor(model => model.PhoneNumber)
.NotEmpty();
}
}
Controller code to validate the view models:
private bool IsModelStateValid(PersonForm model)
{
// The `repository` field is an IPersonRepository object from the DI container
var validator = new PersonFormValidator(repository);
var results = validator.Validate(model);
if (results.IsValid)
return true;
results.AddToModelState(ModelState, "");
return false;
}
Razor template code to render the page
Page level template
#model PersonForm
#Html.EditorFor(model => model.Phones)
PhoneFieldCollection editor template
#model PhoneFieldsCollection
<fieldset class="form-group form-group-phones">
<legend class="control-label col-md-3 required">
Phone Numbers:
</legend>
<div class="col-md-9">
#Html.ValidationMessageFor(model => model, "", new { role = "alert", #class = "alert alert-danger", #for = Html.IdFor(model => model) + "-addButton" })
<ol class="list-unstyled">
#foreach (var item in Model.Items)
{
if (item.Value.IsClientSideTemplate)
{
<script type="text/html">
#Html.EditorFor(model => model.Items[item.Key])
</script>
}
else
{
#Html.EditorFor(model => model.Items[item.Key])
}
}
</ol>
<hr />
<p>
<button type="button" class="btn btn-default" id="#Html.IdFor(model => model)-addButton"
data-dynamiclist-action="add"
data-dynamiclist="fieldset.form-group-phones ol">
<span class="glyphicon glyphicon-plus"></span>
Add another phone number
</button>
</p>
</div>
</fieldset>
PhoneNumberFields editor template
#model PhoneNumberFields
#Html.EditorFor(model => model.PhoneNumber)
#Html.ValidationMessageFor(model => model.PhoneNumber)
Required field message not showing up
When I POST the form back to the server with the phone number field empty, I get a validation summary message at the top of the page saying "Phone number field is required", which is what I expect. However, the call to ValidationMessageFor(model => model.PhoneNumber) in the editor template is not causing the validation message to appear by the form field.
When running the application in debug mode I get Phones[0].PhoneNumber for the name of the field that has the validation message, but the name of the field in the view model is Phones.Items[123].PhoneNumber (where 123 is a database Id, or a timestamp generated by new Date().getTime() in JavaScript).
So I know why the validation message isn't showing up next to the field. The challenge is, how can I do this?
How can I validate a Dictionary with FluentValidation so the error messages appear by the form fields when using **ValidationMessageFor(model => model.PhoneNumber) in the editor template?
Update: Looks like there is a GitHub issue from 2017 related to this: Support for IDictionary Validation. The person found a workaround, but the maintainer for FluentValidation basically said supporting this is a monster pain and would require a major refactoring job. I might try fiddling with this myself and posting an answer if I can get something to work.
I am new to MVC and trying to understand viewmodels.
I have Staff, Service, BookingSlot, Appointments and the ApplicationUser entities. I have the following viewmodel:
public class AppointmentBookingViewModel
{
[Display (Name ="Select Staff")]
public int StaffId { get; set; }
public IEnumerable<Staff> Staffs { get; set; }
[Display(Name = "Select Service")]
public int ServiceId { get; set; }
public IEnumerable<Service> Services { get; set; }
[Display(Name = "Select Slot")]
public int BookingSlotId { get; set; }
public IEnumerable<BookingSlot> BookingSlots { get; set; }
}
This is the controller:
public class AppointmentBookingController : Controller
{
private readonly SalonContext _context;
private AppointmentBookingViewModel _appointmentBookingViewModel = new AppointmentBookingViewModel();
public AppointmentBookingController(SalonContext context)
{
_context = context;
ConfigureViewModel(_appointmentBookingViewModel);
}
public void ConfigureViewModel(AppointmentBookingViewModel appointmentBookingViewModel)
{
appointmentBookingViewModel.Staffs = _context.Staffs;
appointmentBookingViewModel.Services = _context.Services;
appointmentBookingViewModel.BookingSlots = _context.BookingSlots;
}
// GET: AppointmentBooking
public ActionResult Index()
{
return View(_appointmentBookingViewModel);
}
}
My question is, how can I create a form in the view and post the data to the Appointments table, the following doesn't work.
#model HairStudio.Services.ViewModels.AppointmentBooking.AppointmentBookingViewModel
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col-12">
<form asp-action="Create">
<div class="form-group">
<label asp-for="ServiceId" class="control-label"></label>
<select asp-for="ServiceId" class="form-control"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
You already directed your form to action called "Create" with asp-action attribute, but there is no such action in your controller. Submitting a form sends a HTTP POST request, which needs to be handled by your controller. Therefore, add a Create() method in your AppointmentBookingController:
// POST: Create
public IActionResult Create(AppointmentBookingViewModel appointmentViewModel)
{
if (!ModelState.IsValid)
{
// Server side validation of form has failed.
// Return to the calling view and inform the user about the errors.
return View(appointmentViewModel, "Index");
}
return View(appointmentViewModel, "<NAME_OF_YOUR_CREATED_APPOINTMENT_VIEW>");
}
Consider redirecting after successfully accepting a HTTP POST request according to a design pattern Post/Redirect/Get.
Also, take a look at this part of ASP.NET Core documentation about working with forms. I'm sure you'll find there something of value.
There's nothing magical about a view model. It's just a class. The idea is that the entity class (i.e. the thing you're persisting to the database via Entity Framework) should be concerned only with the needs of the database. A view can and often does have an entirely different set of needs, so you create a class specifically for that: the view model. This is just basic SRP (single-responsibility principle): a single class shouldn't try to do too much.
Then, you simply need a way to bridge the two. In other words, you need to copy values from the entity to the view model and vice versa. That process is called mapping, and can be achieved in a number of different ways. The most common approach is to use a third-party library like AutoMapper. However, you can also just manually map over each value or even use something akin to the factory pattern, where you have another class that holds the knowledge for how to do the mapping and can spit out an entity from a view model and vice versa.
Now, it's not really possible to give you exact guidance because we don't have your entity(ies), but you seem to be wanting to pick a particular Staff, Service and BookingSlot and associate that with the Appointment you're creating. It's not critical, but for efficiency, you should not be carrying around the full set of all these entities on your view model. All you need is an IEnumerable<SelectListItem>, which allows you to use much more efficient queries:
Instead of the Staffs property, for example:
public IEnumerable<SelectListItem> StaffOptions { get; set; }
Then:
model.StaffOptions = await _context.Staffs.AsNoTracking()
.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() })
.ToListAsync();
In your view:
<select asp-for="StaffId" asp-items="#Model.StaffOptions" class="form-control"></select>
Please see the models below:
public class Apple //: Fruit
{
public string Description { get; set; }
public int Id { get; protected set; }
}
public class AppleModel
{
public int Id { get; set; }
public string Description { get; set; }
}
and the controller below:
[HttpPost]
public ActionResult Index(Apple apple)
{
return View();
}
[HttpGet]
public ActionResult Index()
{
var AppleModel = new AppleModel();
AppleModel.Id = 1;
AppleModel.Description = "Apple";
var Apple = AutoMapper.Mapper.Map<Apple>(AppleModel);
return View("View1",Apple);
}
and the view below:
#model PreQualification.Web.Controllers.Apple
#{
ViewBag.Title = "View1";
}
<h2>View1</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>AppleModel</h4>
<hr />
#Html.ValidationSummary(true)
<div class="form-group">
#Html.LabelFor(model => model.Id)
<div class="col-md-10">
#Html.EditorFor(model => model.Id)
#Html.ValidationMessageFor(model => model.Id)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Description, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Apple.Id is 0 in the HttpPost method because it is a protected variable in the model. Is there anyway around this?
The reason I ask is because I am trying to use this as a model: https://github.com/nhibernate/NHibernate.AspNet.Identity/tree/master/source/NHibernate.AspNet.Identity and the id contained in the superclass is protected.
There are a couple ways around this, including a child class that implements a public whose setter applies the value to the protected id field.
However these are bandaids. Generally these problems are encountered because of a difference in how people view models. Either it is a reusable Data Transfer Object or it is not.
In the world where it is not, you have to shoehorn business objects into bindable models and always run into these weird problems.
In the world where they are, they are custom tailored to fit the data needs and are mapped into business objects with something like an automapper. More importantly, by making a model to fit this request, you protect against attacks on accidentally exposed parameters.
If the makeshift business object has public properties that change your behavior, they can be exploited by sending additional parameters back with the post request.
I know this does not specifically answer your question, but following the path where models are not DTO's is probably not the right answer either.
As elaborated by peewee_RotA, it's better to separate out the conceptual differences between a model used for a view and a domain model that will actually perform an action or is used to directly perform an action. To that end, you need a view model such as Apple
public class Apple
{
public string Description { get; set; }
public int Id { get; set; }
}
In your post action, you'd have;
[HttpPost]
public ActionResult Index(Apple apple)
{
// translate your view model to your domain model
AppleModel model = new AppleModel(apple.Id, apple.Description);
model.DoStuff();
}
In terms of NHiberhate though, it's not easy to instantiate the required object with the view model's ID and this is the crux of your issue I think. You're trying to use your domain model as a view model and the domain model is locked down...legitimately. It's job is to control how IdentityUser and the like are instantiated as they're meant to be generated by the associated factory classes, not MVC's model binder.
To that end, leave your view model as simple as possible and leverage NHibernate's factory classes to create the necessary Identity objects by looking up the ID. This link may shed some light on how to look up the user ID passed in by Apple.Id.
EDIT
I've done a little more digging on looking up the entity in NHibernate and the following post seems to do the basics. Does this get what you need?
I have a list of Objects which I want to modify on my View, I have built a View Model to contain the list of objects:
public class TrainerListViewModel
{
public List<Trainer> Trainers { get; set; }
}
and I send a list of Trainers to the view from the controller:
public virtual ActionResult Social()
{
var Trainers = new TrainerListViewModel();
Trainers.Trainers = (from t in _db.Trainers select t).ToList();
return View(Trainers);
}
and here is my view:
#model XStreamingFitness.ViewModels.TrainerListViewModel
#using (Html.BeginForm("Social", "Participant", FormMethod.Post))
{
for (int i = 0; i < Model.Trainers.Count; i++)
{
<div class="formSep">
<div class="editor-label">
#Html.LabelFor(m => m.Trainers[i].permissionPostFacebook)
</div>
<div class="editor-field">
#Html.EditorFor(m => m.Trainers[i].permissionPostFacebook)
#Html.ValidationMessageFor(m => m.Trainers[i].permissionPostFacebook)
</div>
</div>
}
<input type="submit" value="Save Settings" name="Submit" />
}
now here is the POST controller method:
[HttpPost]
public virtual ActionResult Social(TrainerListViewModel Trainers)
{
return RedirectToAction("Profile");
}
but everytime I submit, the Trainers model is empty and I am not sure why this could be happening.
This question has been asked before here at SO I suggest you check mvc3 submit model empty, its the same principals as the problem you are having.
See the snippet from the post.
I see people writing the following lambda expression modelItem =>
item.SomeProperty in their views very often and asking why the model
binder doesn't correctly bind collection properties on their view
models.
This won't generate proper name for the checkbox so that the default
model binder is able to recreate the Settings collection. I would
recommend you reading the following blog post to better understand the
correct format that the model binder expects.
-By Darin Dimitrov
It is to do with the way you are building your form he goes to suggest you use Property Editor Templates for the Trainer object.
This should then work.
Hope it helps
I am new to MVC3 and am trying to write a blog application as a learning tool.
I've created a database object for the blog post and generated a controller using the Controller with Read/Write actions and views using Entity Framework to control the entity.
I'm having troubles with the edit commands. There are about 6 properties for a blog post but I only want to allow the edit to modify the title and content of the post. My code is as follows:
public ActionResult Edit(int id)
{
blog_Post blog_post = db.blog_Post.Find(id);
return View(blog_post);
}
//
// POST: /Post/Edit/5
[HttpPost]
public ActionResult Edit(blog_Post blog_post)
{
if (ModelState.IsValid)
{
db.Entry(blog_post).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(blog_post);
}
#model BlogVersion1._0.blog_Post
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>blog_Post</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.PostContent)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.PostContent)
#Html.ValidationMessageFor(model => model.PostContent)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
The problem that comes about is in the public ActionResult Edit(blog_Post blog_post) method. In the Edit(int id) method, I have put a breakpoint in and I can see that blog_post is being properly passed to the view (including all of its properties populated).
But the blog_post being returned to the [HttpPost] method is missing properties for UserId, DateCreated, etc. An exception is obviously thrown on the db.SaveChanges call as required foreign keys are missing.
How do I ensure that all properties are returned to the second edit method to properly make the update?
Because you are not sending the values of those elements from your form when you do the POST ing. One way to fix this is to keep them inside the form using Hidden Variables
#using(Html.BeginForm())
{
#Html.EditorFor(model => model.Title)
#Html.HiddenFor(model => model.UserId)
<input type="submit" />
}
I think the clean solution is to "Dont use the Domain model in the view, Use a ViewModel with necessary properties for the View. In this case, obviously CreatedDate should not be something the View should supply. It should be something the code will be filled to the object.
So create a ViewModel for this
public class BlogPostViewModel
{
public int ID { set;get;}
public string Title { set;get;}
public string Description { set;get;}
}
and use this for transfering data from View to controller and viceversa
public ActionResult Edit(int id)
{
var domainObject=repo.GetPost(id);
var viewModel=new BlogPostViewModel();
viewModel.ID=domainObject.ID;
viewModel.Title=domainObject.Title;
//map other REQUIRED properties also
return View(viewModel);
}
Your view will be strongly typed to this
#model BlogPostViewModel
#using(Html.BeginForm())
{
#Html.EditorFor(model => model.Title)
#Html.HiddenFor(model => model.Description)
<input type="submit" />
}
In the POST action,map it back to the domain object and save
[HttpPost]
public ActionResult Edit(BlogPostViewModel model)
{
if(ModelState.IsValid)
{
var domainObject=new blog_Post();
domainObject.Title=model.Title;
domainObject.ModifiedDate=DateTime.Now;
//set other properties also
repo.Update(domainObject);
return RedirecToAction("Success");
}
return View(model);
}
Instead of manually mapping properties one by one you can consider using AutoMapper library which does this for you in one line of code!
Just add hidden fields for all other, non-editable properties.
#Html.HiddenFor(model => model.Id)
These field will be included in POST and hence, model binder will correctly put them into your blog_post instance.
On the other side, you should really be using view models - simple POCO classes that will be models for your views. Using entity models directly is not recommended.
Here's some info on that:
ASP.NET MVC ViewModel Pattern
http://stephenwalther.com/archive/2009/04/13/asp-net-mvc-tip-50-ndash-create-view-models.aspx
The model binder will only populate the properties that are POSTed in the HTTP request. Your view only contains Title and PostContent.
You either need to include hidden fields for each of the missing properties. Or just the ID property and then do a database lookup for the rest.
For your case, I think You should rather use the HtmlHelper extension method "EditorForModel" instead of calling "EditorFor" for each property. You are complicating your life using EditorFor on each property (and as gred84 is saying, it doesn't post the non displayed properties in the HTTP request in you context).
In your blog_Post model class you should flag each property that you don't want to be edited with the attribute [HiddenInput(DisplayValue = false)]
Then instead of all your code in the view, you can simply have (simplified - without validation summary)
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" value="Save" />
}