I'm new to asp.mvc and I'm trying to figure out why when I try to update values of a model through EditorFor helpers it won't send the updated value.
I have to follow a strict code convention and my parameters must start with a prefix (bln/str/int and so on)
If I leave the other parameters (permissionKey/stationKey/username) without the prefix, the values received are the updated one (the ones I need).
[HttpPost, ActionName("Save")]
public ActionResult Save(int intId, string permissionKey, string stationKey, string username)
{
var perm = _repository
.Get(row => row.Id == intId)
.First();
perm.PermissionKey = permissionKey;
perm.StationKey = stationKey;
perm.Username = username;
If I change the definition to strPermissionKey/strStationKey/strUsername, I will receive the old values of the model.
Model:
public class EditablePermissionRowModel
{
public int Id { get; set; }
public string PermissionKey { get; set; }
public string StationKey { get; set; }
public string Username { get; set; }
public bool EditPressed { get; set; } = false;
}
my Html looks like this:
#using (Html.BeginForm("Save", "Permissions"))
{
#*#Html.AntiForgeryToken()*#
#Html.HiddenFor(m => m.Id)
.... more html....
<button type="submit" class="btn btn-link">
<span class="glyphicon glyphicon-ok"></span>
</button>
I also tried to include all parameters that I need as hidden.
#Html.Hidden("strPermissionKey", Model.PermissionKey);
But this still didn't work. (Also tried #Html.Editor("strPermissionKey", Model.PermissionKey) but not luck)
What am I missing?
EDIT:
#StephenMuecke helped and pointed out that I only need to pass the model back as a parameter... and this did it. Problem solved.
this:
public ActionResult Save(EditablePermissionRowModel udtModel) {
... code ....
Related
I am trying to create a blog site with asp.net core 3.1. I want to add a comment to the blog post, but I cannot get a query string to which blog post I posted.
I shared my codes as follows. Now, what is the reason why the id from the query string always returns 0?
Or what is the situation I am constantly doing wrong?
There is probably a very simple solution but it took me all day please can you help?
Entity;
public class Content : IEntity
{
public int Id { get; set; }
public int CategoryId { get; set; }
public int UserId { get; set; }
public virtual List<ContentComment> ContentComments { get; set; }
}
public class ContentComment : IEntity
{
public int id { get; set; }
public int UserId { get; set; }
public int ContentId { get; set; }
public string Comment { get; set; }
public DateTime CommnetCreateTime{ get; set; }
public virtual Content Contents { get; set; }
}
Controller :
public class BlogContentController : Controller
{
private IBlogService _blogService;
private IContentCommentsServices _contentCommentsServices;
public BlogContentController(IBlogService blogService, IContentCommentsServices contentCommentsServices)
{
_blogService = blogService;
_contentCommentsServices = contentCommentsServices;
}
public IActionResult Index(int id)
{
var blogModel = new ContentViewModel
{
Blogs = _blogService.GetById(id)
};
return View(blogModel);
}
public IActionResult CommentAdd()
{
var model = new ContentCommendViewModel()
{
ContentComments = new List<ContentComment>()
};
return Ok();
}
[HttpPost]
public IActionResult CommentAdd(ContentComment contentComment)
{
contentComment.ContentId = Convert.ToInt32(HttpContext.Request.Query["id"]);
_contentCommentsServices.Add(contentComment);
return RedirectToAction("CommentAdd");
}
}
View Page :
<div class="media-body mt-2" id="yorumyap">
<h5>Yorum Bırakın</h5>
<form asp-controller="BlogContent" asp-action="CommentAdd">
<div class="form-group">
<textarea name="Comment" class="form-control form-control-sm" rows="3" style="resize: none;" id="commenttext"></textarea>
</div>
<div class="text-right">
<button type="submit" class="btn btn-tekisimbilsim mb-2" id="commendAdd">Yorum Yap</button>
</div>
</form>
</div>
You are trying to get the 'id' from the query string of the request. This would require your post URL to look something like: https://example.com/BlogContent/CommentAdd?id=42. Based on the HTML you have provided, I bet your URL looks like: https://example.com/BlogContent/CommentAdd (you can determine what your URL looks like by using your browsers inspection tools to look at the form tag in the HTML). In order to get the desired behavior you will need to update your form tag to look something like this:
<form asp-controller="BlogContent" asp-action="CommentAdd" asp-route-id="#Model.BlogId">
the important part in this is the addition of the 'asp-route-id' tag helper. You can find information about this tag helper (listed as 'asp-route-{value}') and others at:
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-form-tag-helper
But wait, there's more! In this example I just gave more than likely your URL will turn out to be https://example.com/BlogContent/CommentAdd/42 (I say more than likely because this is determined by your route configuration). This will not give you the expected result and the id will again be zero! So you have a couple of options. The easiest is to change the 'asp-route-{value}' tag helper to be something like 'asp-route-contentId'. This would give you html that looks like:
<form asp-controller="BlogContent" asp-action="CommentAdd" asp-route-contentId="#Model.BlogId">
which would generate a URL that would be https://example.com/BlogContent/CommentAdd?contentId=42. You would then need to update your controller code to retrieve the id to look like this:
contentComment.ContentId = Convert.ToInt32(HttpContext.Request.Query["contentId"]);
There are a myriad of other options we could go into but I will stop this essay for the time being here.
I have searched around and not had much luck finding a solution to my exact problem.
Model
public class PageDetailsViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
}
Controller
public ActionResult Search(int SysID)
{
var query = from r in _db.Auctions
from d in _db.Product_Details
where SysID == d.Id && r.BidStatus == "Open" && d.Id == r.Product_DetailsId
select new PageDetailsViewModel
{
Name = d.Name,
Description = d.Description,
Image = d.Image
};
return View(query);
}
View
#model IEnumerable<ProjectT.Models.PageDetailsViewModel>
#Html.DisplayNameFor(x => x.Name)
This fails to bring the name through. However, if I use a foreach
#foreach (var item in Model)
{
#item.Name
}
It brings through the name no problem.
Any help is much appreciated.
This extension method shows the value of the DisplayNameAttribute from DataAnnotations namespace. Consider this a label. Typically it is used like this:
[DisplayName("The Name")]
public string Name { get; set; }
And in the view:
#Html.DisplayNameFor(x => x.Name) <-- displays The Name
The code above will work only if the model is a single item. For the list case, as you have, you need to do some tricks, say a for loop, so you could do something like:
#Html.DisplayNameFor(x => x[i].Name): #Model[i].Name <-- displays The Name: Bill
Please bear with my noobness, I'm super new to the MVC pattern.
What I'm trying to do
I am building a profile information page for registered users on my site. This page would list data about the user, such as date of birth, telephone number, subscription status, etc.. You get the idea. I would also like to have a form to let users change their password, email address, personal information on the same page.
My problem
The user's data comes from my controller via a passed model variable:
public ActionResult Profil()
{
var model = db.Users.First(e => e.UserName == WebSecurity.CurrentUserName);
return View(model);
}
The output looks like this in my view:
<label>Phone number: </label>
#if (Model.PhoneNumber != null)
{
#Model.PhoneNumber
}
else
{
<span class="red">You haven't set up your phone number yet. </span>
}
The form in which the user could change his info would use another model, ProfileModel. So basiccaly I need to use two models in my view, one for outputting information and one for posting data. I thought that using a partial view I can achieve this, but I get this error:
The model item passed into the dictionary is of type
'Applicense.Models.User', but this dictionary requires a model item of
type 'Applicense.Models.ProfileModel'.
Here's what my call to the partial view looks like:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
#Html.Partial("_ModifyProfileInfo")
}
Here's the partial view:
#model Applicense.Models.ProfileModel
<ul>
<li>
#Html.LabelFor(m => m.Email)
#Html.EditorFor(m => m.Email)
</li>
<li>
#Html.LabelFor(m => m.ConfirmEmail)
#Html.EditorFor(m => m.ConfirmEmail)
</li>
<input type="submit" value="Update e-mail" />
</ul>
And finally here's my ProfileModel:
public class ProfileModel
{
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "New e-mail address")]
public string Email { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name = "Confirm new e-mail address")]
[Compare("Email", ErrorMessage = "The e-mail and it's confirmation field do not match.")]
public string ConfirmEmail { get; set; }
}
Am I missing something? What's the proper way to do this?
Edit:
I remade my code reflecting Nikola Mitev's answer, but now I have another problem. Here's the error I get:
Object reference not set to an instance of an object. (#Model.UserObject.LastName)
This only occurs when I'm posting the changed e-mail address values. Here's my ViewModel (ProfileModel.cs):
public class ProfileModel
{
public User UserObject { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Új e-mail cím")]
public string Email { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name = "Új e-mail cím megerősítése")]
[Compare("Email", ErrorMessage = "A két e-mail cím nem egyezik.")]
public string ConfirmEmail { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name= "E-mail cím")]
public string ReferEmail { get; set; }
}
Controller:
public ActionResult Profil()
{
var User = db.Users.First(e => e.UserName == WebSecurity.CurrentUserName);
var ProfileViewModel = new ProfileModel
{
UserObject = User
};
return View(ProfileViewModel);
}
And finally here's my user.cs model class:
[Table("UserProfile")]
public class User
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
[Column("UserName")]
public string UserName { get; set; }
[Column("Email")]
[Required]
public string Email { get; set; }
[Column("FirstName")]
public string FirstName { get; set; }
[Column("LastName")]
public string LastName { get; set; }
[Column("PhoneNumber")]
public string PhoneNumber { get; set; }
... You get the idea of the rest...
I'm thinking it's happening because the model is trying to put data in each required columns into the database.
Edit2:
The httppost method of my Profil action:
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Profil(ProfileModel model)
{
if (ModelState.IsValid)
{
//insert into database
return Content("everything's good");
}
else
{
//outputs form errors
return View(model);
}
}
The best way to handle this situation is to use and pass viewModel to your Profile controller, viewModel is wrapper class for multiple objects that you want to pass to your view.
public class ProfileUserViewModel
{
public ProfileModel ProfileModelObject {get; set;}
public UserModel UserModelObject {get; set;}
}
Your controller should look like:
public ActionResult Profil()
{
var profileModel = db.Users.First(e => e.UserName == WebSecurity.CurrentUserName);
var userModel = //fetch from db.
var pmViewModel = new ProfileUserViewModel
{
ProfileModelObject = profileModel,
UserModelObject = userModel
};
return View(pmViewModel);
}
And finally your view :
#model Applicense.Models.ProfileUserViewModel
<label>Phone number: </label>
#if (Model.ProfileModelObject.PhoneNumber != null)
{
#Model.PhoneNumber
}
else
{
<span class="red">You haven't set up your phone number yet. </span>
}
There is an overload of #Html.Partial which allows you to send ViewData as defined in your controller - this is the method I generally use for partial views.
In your controller define ViewData["mypartialdata"] as ViewDataDictionary. Then in your view
#Html.Partial("_ModifyProfileInfo",ViewData["mypartialdata"])
In your [HttpPost] profil function, if modelstate.isvalid is false, you return your edit view, but you need to define your pmViewModel again , other wise your partial view will not have an object to display. Try using the following and let us know what happens
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Profil(ProfileModel model)
{
if (ModelState.IsValid)
{
//insert into database
return Content("everything's good");
}
else
{
//outputs form errors
var pmViewModel = new ProfileUserViewModel
{
ProfileModelObject = profileModel,
UserModelObject = userModel
};
return View(model);
}
}
While I know this question has been asked longtime ago however some people might still face a similar problem. One easy solution I use to pass or have more than one view model on a page is to use a ViewBag to hold the second object and refer to it in the view. See example bellow.
In your controller do this:
Obj2 personalDets = new Obj2();
DbContext ctx = new DbContext();
var details = ctx.GetPersonalInformation;
foreach(var item in details) {
personalDets.Password = item.Password;
personalDets .EmailAddress = item.EmailAddress;
}
ViewBag.PersonalInformation = personalDets;
Then in your view those properties become readily available for you
I have a simple model which uses a multi select listbox for a many-many EF relationship.
On my Create action, I'm getting the error
The parameter conversion from type 'System.String' to type 'MyProject.Models.Location' failed because no type converter can convert between these types.
I have 2 models, an Article and a Location:
Article.cs
namespace MyProject.Models
{
public class Article
{
public Article()
{
Locations = new List<Location>();
}
[Key]
public int ArticleID { get; set; }
[Required(ErrorMessage = "Article Title is required.")]
[MaxLength(200, ErrorMessage = "Article Title cannot be longer than 200 characters.")]
public string Title { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
Location.cs:
namespace MyProject.Models
{
public class Location
{
[Key]
public int LocationID { get; set; }
[Required(ErrorMessage = "Location Name is required.")]
[MaxLength(100, ErrorMessage = "Location Name cannot be longer than 100 characters.")]
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
}
}
I have a ViewModel:
namespace MyProject.ViewModels
{
public class ArticleFormViewModel
{
public Article article { get; set; }
public virtual List<Location> Locations { get; set; }
public ArticleFormViewModel(Article _article, List<Location> _locations)
{
article = _article;
Locations = _locations;
}
}
}
create.cshtml:
#model MyProject.ViewModels.ArticleFormViewModel
<h2>Create</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Article</legend>
<div class="editor-label">
#Html.LabelFor(model => model.article.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.article.Title)
#Html.ValidationMessageFor(model => model.article.Title)
</div>
<h3>Locations</h3>
#Html.ListBoxFor(m=>m.article.Locations,new MultiSelectList(Model.Locations,"LocationID","Name"))
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Finally my controller actions:
// GET: /Article/Create
public ActionResult Create()
{
var article = new Article();
var AllLocations = from l in db.Locations
select l;
ArticleFormViewModel viewModel = new ArticleFormViewModel(article, AllLocations.ToList());
return View(viewModel);
}
//
// POST: /Article/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Article article)
{
var errors = ModelState.Values.SelectMany(v => v.Errors);
if (ModelState.IsValid)
{
var locations = Request.Form["article.Locations"];
if (locations != null)
{
var locationIDs = locations.Split(',');
foreach (var locationID in locationIDs)
{
int id = int.Parse(locationID);
Location location = db.Locations.Where(l => l.LocationID == id).First();
article.Locations.Add(location);
}
}
db.Articles.Add(article);
db.SaveChanges();
return RedirectToAction("Index");
}
var AllLocations = from l in db.Locations
select l;
ArticleFormViewModel viewModel = new ArticleFormViewModel(article, AllLocations.ToList());
return View(viewModel);
}
This all works relatively well, my Locations listbox is populated properly:
If I do not select a Location then my model is saved properly. If I select one or more locations then my Model.IsValid check fails with the exception
The parameter conversion from type 'System.String' to type 'MyProject.Models.Location' failed because no type converter can convert between these types.
However if I remove the ModelState.IsValid check then despite the error my values are all correctly saved into the database - just that I lose validation for things such as the model title.
Hope someone can help!
Unless you create a type converter, you cannot directly bind the results of your list box directly to a complex object like that. The reason lies in the fact that MVC can only deal with posted HTTP values, which in this case are an array of strings that contain the selected ID's. Those strings do not directly map to your Locations object (ie the number 1 cannot be directly converted to a Locations object with an ID of 1).
Your best bet is to have a list of location ID's in your View Model of type string or int to accept the posted values, then in your post method create the Location objects and fill them with the correct ID's.
FYI, the reason your code works is because you are bypassing the model binding and going directly to the Request.Form collection. You will notice that the bound Article object will not have any Location objects.
EDIT:
I don't even see how your code would work even without this problem. Your ArticleFormViewModel does not have a parameterless constructor, so that will fail in model binding (unless you have a custom model binder).
In any event, what you want to do is this (note, you will have to populate SelectedLocationIDs if you want them to be selected when the view is rendered):
public class ArticleFormViewModel
{
...
List<int> SelectedLocationIDs { get; set; }
...
}
Then, in your view you have:
#Html.ListBoxFor(m=>m.SelectedLocationIDs,
new MultiSelectList(Model.Locations,"LocationID","Name"))
In your Post method, instead of the code that calls Request.Form, you have something like this:
foreach(var locationID in article.SelectedLocationIDs) {
... // look up your locations and add them to the model
}
I am having this issue at the moment, I had address model (use required attribute to decorate) which can be used more than once on the same page, one is billing address and the other one is shipping address. when validation failed, I'd like to have suffix in front of my generic error message indicate which address is required e.g. "{0} - address line 1 required", either billing or shipping
Here is my model
public class AddressBaseModel
{
[Display(Name="Address line 1")]
[Required(ErrorMessageResourceType = typeof(ModelValidation), ErrorMessageResourceName = "AddrLine1Required")]
public string AddressLine1 { get; set; }
[Display(Name="Address line 2")]
[Required(ErrorMessageResourceType = typeof(ModelValidation), ErrorMessageResourceName = "AddrLine2Required")]
public string AddressLine2 { get; set; }
[Display(Name="Address line 3")]
public string AddressLine3 { get; set; }
[Display(Name="Address line 4")]
public string AddressLine4 { get; set; }
}
}
Here is the code segment I used in my page
<fieldset class="space-bottom">
<legend>Please enter your home address</legend>
<div id="home_fields">
#Html.EditorFor(m => m.HomeAddress)
</div>
</fieldset>
<fieldset class="space-bottom">
<legend>Please enter your delivery address</legend>
<div id="delivery_fields">
#Html.EditorFor(m => m.DeliveryAddress)
</div>
</fieldset>
Thanks
Personally I use the FluentValidation.NET library instead of Data Annotations as it makes things so much easier and provides a lot more power. Here's an example of how to achieve your goal using this ilbrary.
Create a new ASP.NET MVC 3 project using the default Visual Studio template
Install the FluentValidation.MVC3 NuGet package.
Add the following line to Application_Start:
ModelValidatorProviders.Providers.Add(
new FluentValidationModelValidatorProvider(
new AttributedValidatorFactory()
)
);
Define the following models:
public class AddressBaseModel
{
public string AddressLine1 { get; set; }
}
[Validator(typeof(MyViewModelValidator))]
public class MyViewModel
{
public AddressBaseModel HomeAddress { get; set; }
public AddressBaseModel DeliveryAddress { get; set; }
}
And the following Validators:
public class AddressBaseModelValidator : AbstractValidator<AddressBaseModel>
{
private readonly string _addressType;
public AddressBaseModelValidator(string addressType)
{
_addressType = addressType;
RuleFor(x => x.AddressLine1)
.NotEmpty()
.WithMessage(string.Format("{0} - address line 1 required", addressType));
}
}
public class MyViewModelValidator : AbstractValidator<MyViewModel>
{
public MyViewModelValidator()
{
RuleFor(x => x.HomeAddress)
.SetValidator(new AddressBaseModelValidator("billing"));
RuleFor(x => x.DeliveryAddress)
.SetValidator(new AddressBaseModelValidator("shipping"));
}
}
Modify the HomeController:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
HomeAddress = new AddressBaseModel(),
DeliveryAddress = new AddressBaseModel()
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
And the corresponding Index.cshtml view:
#model MyViewModel
#using (Html.BeginForm())
{
<fieldset class="space-bottom">
<legend>Please enter your home address</legend>
<div id="home_fields">
#Html.EditorFor(m => m.HomeAddress)
</div>
</fieldset>
<fieldset class="space-bottom">
<legend>Please enter your delivery address</legend>
<div id="delivery_fields">
#Html.EditorFor(m => m.DeliveryAddress)
</div>
</fieldset>
<input type="submit" value="Register" />
}
You could create a custom attribute that does the dynamic formatting for you. You would just tag your address fields with the Address attribute like this:
[Address]
public string AddressLine1 { get; set; }
You would need to add a property in the AddressBaseModel where you tell the system what type of address this is (you would set this to "Billing" or "Shipping" when you instantiate the view model right before you pass the view model to the View in the controller get action):
public string AddressType { get; set; }
A custom attribute like this should work (I haven't tested it, I wrote it just now). This automatically gets the address type you specified when you create the model instance and formats it with the display name of the address field).
public class AddressAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "{0} - {1} required";
public AddressAttribute()
: base(DefaultErrorMessage) { }
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value != null)
{
if (!base.IsValid(value))
{
// get the property called "AddressType" from the model so we know if it's Billing or Shipping
var addressType = validationContext.ObjectInstance.GetType()
.GetProperty("AddressType")
.GetValue(validationContext.ObjectInstance, null);
// use the display name of the address field in the error message
return new ValidationResult(
string.Format(DefaultErrorMessage, addressType, validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
}
This should work:
[Required(ErrorMessage = "The Address 2 is required.")]