Model Binding Error for Parent Object Entity - asp.net-core

Using latest ASP.NET Core, Windows 10 Pro
TLDR: my controller is returning a "Microsoft.AspNetCore.Mvc.ModelBinding.ModelError" for an entity's parent.
Entity: Schedule
public class Schedule
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ScheduleId { get; set; }
[Required]
[Display(Name = "Start On")]
public DateTime StartDateTime { get; set; } = DateTime.Now.AddMinutes(1);
[Display(Name = "Reschedule On")]
public string Recurrence { get; set; }
[Display(Name = "Parameter 1")]
[MaxLength(256), StringLength(256)]
public string Param1 { get; set; }
// ... more properties
[Display(Name = "Parameter 7")]
[MaxLength(256), StringLength(256)]
public string Param7 { get; set; }
[Display(Name = "Message Text")]
public string MessageText { get; set; }
#region relationships
[Required]
[Display(Name = "For Process")]
public int ProcessId { get; set; }
[Required]
[Display(Name = "For Process")]
public virtual Process Process { get; set; }
public virtual ICollection<ScheduleNotification> ScheduleNotifications { get; set; }
#endregion
}
Entity: Process
public class Process : IValidatableObject
{
private int? _ApplicationId;
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int ProcessId { get; set; }
[Required]
[StringLength(80, MinimumLength = 5, ErrorMessage = "Names must be between 5 and 80 characters")]
[MaxLength(80)]
public string Name { get; set; }
public string Description { get; set; }
[Required]
[Display(Name = "Process Type")]
public ProcessTypeEnum ProcessType { get; set; }
// ... more properties
[Display(Name = "Primary Application")]
public int? ApplicationId
{
get
{
return _ApplicationId;
}
set
{
_ApplicationId = value == 0 ? null : value;
}
}
public Application Application { get; set; }
public ICollection<Schedule> Schedules { get; set; }
}
Controller Action on Create Get
public IActionResult Create(int? processId)
{
// no value provided, so redirect to view that prompts for process
if (!processId.HasValue || processId == 0)
{
TempData["Error"] = "You must select a process to schedule";
return RedirectToAction("Processes");
}
var schedule = new Schedule()
{
ProcessId = processId.GetValueOrDefault(),
Process = _context.Processes.Find(processId.GetValueOrDefault()),
ScheduleNotifications = new List<ScheduleNotification>()
};
return View(schedule);
}
Razor view called on create
#model HangfireServer.Models.Schedule
#{
ViewData["Title"] = "Create";
}
<h1>Schedule</h1>
<hr />
<div class="row">
<div class="col">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="text-danger" style="direction:rtl" asp-validation-summary="All"></div>
#Html.HiddenFor(m => m.ProcessId)
#Html.HiddenFor(m => m.Process)
<div class="form-group row">
<text class="control-label col-sm-2">#Html.DisplayNameFor(m => m.Process.Name)</text>
<div class="col-sm-4">
#Html.DisplayFor(m => m.Process.Name)
</div>
<text class="text-left col-sm-6 text-secondary">Process to schedule</text>
</div>
<div class="form-group row">
<label asp-for="StartDateTime" class="control-label col-sm-2"></label>
<div class="col-sm-4">
<input asp-for="StartDateTime" class="control-label" />
<span asp-validation-for="StartDateTime" class="text-danger"></span>
</div>
<text class="text-left col-sm-6 text-secondary">Date and time to initiate process</text>
</div>
<!-- more stuff -->>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
Create post controller
// POST: Schedules/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ScheduleId,StartDateTime,Recurrence,Param1,Param2,Param3,Param4,Param5,Param6,Param7,ProcessId,Process,ScheduleNotifications")] Schedule schedule)
{
if (ModelState.IsValid)
{
// save the new schedule
schedule.Process = await _context.Processes.FindAsync(schedule.ProcessId);
_context.Add(schedule);
await _context.SaveChangesAsync();
// queue the new schedule
EnqueueProcess(schedule);
TempData["Success"] = $"Process {schedule.Process.Name} has been scheduled";
return RedirectToAction(nameof(Index));
}
// display additional errors not caught in page validation
string errorMessage = "";
foreach (var modelState in ViewData.ModelState.Values)
{
foreach (var error in modelState.Errors)
{
errorMessage = String.IsNullOrEmpty(errorMessage) ? error.ToString() : "<br>" + error.ToString();
}
}
TempData["Error"] = errorMessage;
// return to current view
return RedirectToAction("Create", new { processId = schedule.ProcessId });
}
On trace, the GET is passing to the view schedule with its parent process id and process object populated. On POST
ModelState.IsValid = False
schedule.processId = 2 (or whatever the value sent from GET was
schedule.process is null (even though there's a hidden field for process in vie)
ModelState only has 1 invalid key
SubKey={Process}, Key="Process", ValidationState=Invalid

I think I figured it out - but this is just a hack, there's probably a correct way to do it
// populate process property with an instance of a process object
schedule.Process = await _context.Processes.Include(p => p.Application).FirstOrDefaultAsync(p => p.ProcessId == schedule.ProcessId);
ModelState.Clear();
Then either
ModelState.Remove("Process");
Or
TryValidateModel(schedule);
Regardless ModelState.IsValid is now true

Related

Required message is displayed even if the field is provided C#

Using ASP.Net Core, I am developing a simple form to reset the password using email address.
My model is as follow:
public class EmailViewModel
{
[Required(ErrorMessageResourceName = "Model_RequireEmail", ErrorMessageResourceType = typeof(Resources.Identity))]
[Display(Name = "Models_Account_Email", ResourceType = typeof(Resources.Identity))]
[DataType(DataType.EmailAddress)]
[EmailAddress(ErrorMessage = "Models.EmailFormat")]
[MaxLength(100, ErrorMessage = "Models.MaxLength")]
public string? Email { get; set; }
}
And my view:
<form method="post">
<div class="uk-text-danger">
#Html.ValidationSummary()
</div>
<div class="uk-margin">
<div class="uk-inline">
<span class="uk-form-icon" uk-icon="icon: mail"></span>
#Html.TextBoxFor(m => m.Data.EmailViewModel.Email, new
{
#class = "validate uk-input",
placeholder = Html.DisplayNameFor(m => m.Data.EmailViewModel.Email)
})
#Html.HiddenFor(m => m.Data.ResetPasswordAction)
#Html.HiddenFor(m => m.Data.ResetPasswordController)
</div>
</div>
<button class="uk-button uk-button-default">#Html.LocalizedIdentity("Views.Account.PassReset")</button>
</form>
My issue is that even if I provide a correct email address, I keep getting the message of the "required" data annotation. What did I miss?
EDIT 13/12/2021
The issue occurs when I used a nested ViewModel. For example, the viewModel associated with my form is
public class ForgotPasswordViewModel
{
public EmailViewModel EmailViewModel => new EmailViewModel();
[HiddenInput]
public string? ResetPasswordAction { get; set; }
[HiddenInput]
public string? ResetPasswordController { get; set; }
}
using the EmailViewModel above. This give a ModelState.IsValid= false because the email is null even if provided.
If I bypass the EmailViewModel and use for my ForgotPasswordViewMode, the following:
public class ForgotPasswordViewModel
{
[Display(Name = "Models_Account_Email", ResourceType = typeof(Resources.Identity))]
[Required]
[DataType(DataType.EmailAddress)]
[EmailAddress(ErrorMessage = "Models.EmailFormat")]
[MaxLength(100, ErrorMessage = "Models.MaxLength")]
public string? Email { get; set; }
[HiddenInput]
public string? ResetPasswordAction { get; set; }
[HiddenInput]
public string? ResetPasswordController { get; set; }
}
It works nicely.
The problem is that I want to use the nested ViewModel to avoid repeating it.
Why doesn't it work with the nested EmailViewModel?
This is my model and my test view, it will lead to ModelState.IsValid == false because the input form should have a property of emailVM.Email but not EmailViewModel.
public class EmailViewModel
{
//[Required(ErrorMessageResourceName = "Model_RequireEmail", ErrorMessageResourceType = typeof(Resources.Identity))]
//[Display(Name = "Models_Account_Email", ResourceType = typeof(Resources.Identity))]
[Display(Name = "Models_Account_Email")]
[Required]
[DataType(DataType.EmailAddress)]
[EmailAddress(ErrorMessage = "Models.EmailFormat")]
[MaxLength(100, ErrorMessage = "Models.MaxLength")]
public string? Email { get; set; }
}
public class ForgotPasswordViewModel
{
//public EmailViewModel emailVM => new EmailViewModel();
public EmailViewModel emailVM { get; set; }
[HiddenInput]
public string? ResetPasswordAction { get; set; }
[HiddenInput]
public string? ResetPasswordController { get; set; }
}
<form method="POST" asp-action="Edit">
<div class="uk-text-danger">
#Html.ValidationSummary()
</div>
<div class="uk-inline">
<span class="uk-form-icon" uk-icon="icon: mail"></span>
#Html.TextBoxFor(m => m.emailVM.Email, new
{
placeholder = Html.DisplayNameFor(m => m.emailVM.Email)
})
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>

Bind select list to complex object in ASP.NET Core

Using ASP.NET Core Razor Pages, is it possible to bind a select list to a complex object without having an ID property in the PageModel?
I have these model classes:
public class Department
{
[HiddenInput]
public int ID { get; set; }
[Display(Name = "Name")]
[Required(ErrorMessage = "{0} must be specified.")]
[StringLength(50, MinimumLength = 1, ErrorMessage = "{0} has to be {2} to {1} characters long.")]
public string Name { get; set; }
}
public class User
{
[HiddenInput]
public int ID { get; set; }
[Display(Name = "Name")]
[Required(ErrorMessage = "{0} must be specified.")]
[StringLength(50, MinimumLength = 1, ErrorMessage = "{0} has to be {2} to {1} characters long.")]
public string Name { get; set; }
[Display(Name = "Department")]
[Required(ErrorMessage = "{0} must be specified.")]
public Department Department { get; set; }
}
I've create a view for editing a user, including selecting department. This is my PageModel:
public class EditModel : PageModel
{
private readonly IUserRepository _userRepository;
[BindProperty(BinderType = typeof(UserModelBinder))]
public User UserToEdit { get; set; }
public SelectList DepartmentsSL { get; set; }
public EditModel(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public void OnGet(int id)
{
UserToEdit = _userRepository.GetUser(id);
PopulateSelectList();
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
{
PopulateSelectList();
return Page();
}
var success = _userRepository.UpdateUser(UserToEdit);
if (success)
{
return RedirectToPage("./Index");
}
else
{
throw new Exception("Could not save the user.");
}
}
private void PopulateSelectList()
{
var departments = _userRepository.GetAllDepartments();
DepartmentsSL = new SelectList(departments, nameof(Department.ID), nameof(Department.Name), UserToEdit.Department.ID);
}
}
...and this is my view:
#page
#model ProjectName.Pages.Admin.Users.EditModel
#{
ViewData["Title"] = "Edit user";
}
<h1>Edit user</h1>
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input asp-for="UserToEdit.ID" />
<div class="form-group">
<label asp-for="UserToEdit.Name" class="control-label"></label>
<input asp-for="UserToEdit.Name" class="form-control" />
<span asp-validation-for="UserToEdit.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UserToEdit.Department" class="control-label"></label>
<select asp-for="UserToEdit.Department" class="form-control" asp-items="#Model.DepartmentsSL">
<option value="">Choose department</option>
</select>
<span asp-validation-for="UserToEdit.Department" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-page="./Index" class="btn btn-secondary">Cancel</a>
</div>
</form>
As you might have noticed in the PageModel, I'm using a custom modelbinder. I wrote this so that the department of the user is set correctly when the view is posted back.
public class UserModelBinder : IModelBinder
{
private readonly IUserRepository _userRepository;
public UserModelBinder(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var modelName = bindingContext.ModelName;
var user = new User();
// ID.
var idValue = bindingContext.ValueProvider.GetValue(modelName + ".ID");
if (idValue != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(modelName + ".ID", idValue);
var idString = idValue.FirstValue;
if (!String.IsNullOrEmpty(idString))
{
var id = Convert.ToInt32(idString);
user.ID = id;
}
}
// Department.
var departmentValue = bindingContext.ValueProvider.GetValue(modelName + ".Department");
if (departmentValue != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(modelName + ".Department", departmentValue);
var departmentIdString = departmentValue.FirstValue;
if (!String.IsNullOrEmpty(departmentIdString))
{
var departmentId = Convert.ToInt32(departmentIdString);
user.Department = _userRepository.GetDepartment(departmentId);
}
}
bindingContext.Result = ModelBindingResult.Success(user);
return Task.CompletedTask;
}
}
Now, all this seems to work fine - I can change the name and the department and it is posted back correctly. The validation also works. HOWEVER, when opening this edit view the current department of the user is NOT selected in the select list.
In EditModel.PopulateSelectList() I've tried different things, but none seem to work.
DepartmentsSL = new SelectList(departments, nameof(Department.ID), nameof(Department.Name), UserToEdit.Department);
DepartmentsSL = new SelectList(departments, nameof(Department.ID), nameof(Department.Name), UserToEdit.Department.ID);
DepartmentsSL = new SelectList(departments, nameof(Department.ID), nameof(Department.Name), UserToEdit.Department.Name);
Is it possible to solve this last part?
The solution I've seen so far has been to have a separate property "DepartmentID" in the PageModel and bind to that, and then setting the Department property of User after postback. I guess that would also mean I could get rid of the custom model binder, but I'd really like to see if my current solution could be taken all the way. :)
when opening this edit view the current department of the user is NOT selected in the select list
You can try to remove asp-for="UserToEdit.Department" from your SelectTagHelper and manually set name="UserToEdit.Department", like below.
<select name="UserToEdit.Department" class="form-control" asp-items="#Model.DepartmentsSL">
<option value="">Choose department</option>
</select>
And set selectedValue for SelectList, like below.
DepartmentsSL = new SelectList(departments, nameof(Department.ID), nameof(Department.Name), UserToEdit.Department.ID);
Test Result
Besides, another possible approach is creating a custom tag helper to render <select> element and set selected value based on your actual model data and requirement.

Pass common data between views and controllers

I am new to ASP.NET MVC. Want to pass common data to all pages in the application. Following Pass data to layout that are common to all pages. Still not able to get the value in layout and in the controller.
When user logged in to system I set flag 'UserhasLoggedIn' and redirect to another controller action.In the layout, I have added checks if flag is set to false then menu items should not be displayed and LoginPage should display. When page is redirected to Home Index it doesn't get 'UserhasLoggedIn' flag.
Layout.cshtml:
#using WebApplication2.Models
#model ViewModelBase
<div class="btn-toolbar" style="background-color:dimgrey; padding-left:35px;padding-top:-10px;">
#if (null != Model && (bool)Model.IsuserLoggedIn)
{
foreach (var menuItem in Model.MenuItems)
{
<div class="btn-group">
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">menuItem.MenuName <span class="caret"></span></button>
<ul class="dropdown-menu">
#foreach (var subMenu in menuItem.SubMenu)
{
<li>#Html.ActionLink((string)subMenu.MenuName, (string)subMenu.ActionName, (string)subMenu.ControllerName)</li>
}
</ul>
</div>
}
}
<div class="btn-group pull-right" style="padding-right:35px">
#if (null == Model || !(bool)Model.IsuserLoggedIn)
{
<button class="btn btn-primary" onclick="location.href='#Url.Action("Login", "Account")'">Log in</button>
}
else
{
<button class="btn btn-primary" onclick="location.href='#Url.Action("LogOff", "Account")'">Log out </button>
}
</div>
</div>
I have created common view model that can be used.
public abstract class ViewModelBase
{
public bool IsuserLoggedIn;
}
LoginViewModel:
public class LoginViewModel : ViewModelBase
{
[Required]
[Display(Name = "Email")]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
HomeViewModel:
public class HomeViewModel : ViewModelBase
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
AccountController Login Action:
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
return RedirectToAction("Index", "Home", new HomeViewModel() { IsuserLoggedIn = true, MenuItems=null });
}
HomeController Index action: Not able to get the IsUserLoggedIn to true and not even in the Layout.
public ActionResult Index(HomeViewModel baseModel)
{
baseModel.FirstName = "First";
baseModel.LastName = "Last Name";
return View(baseModel);
}
IsuserLoggedIn is a field, not a property and the DefaultModelBinder cannot set its value (it has no setter) so the property is initialized as false in the Index() method (the default value for bool)
Change the base view model to make it a property
public abstract class ViewModelBase
{
public bool IsuserLoggedIn { get; set; }
}
Side note: Just use #if (Model != null && Model.IsuserLoggedIn) - there is no need to cast a bool to a bool

Model evaluating to false while saving to database

So I have this problem when trying to save an item to the database in my asp.net mvc4 web app.
I have three classes listed below
public class Posts
{
[Key]
public int PostID { get; set; }
[Required(ErrorMessage="A Title is required for your Post")]
[Display(Name="Title")]
public string PostTitle { get; set; }
[Required(ErrorMessage="This Field is Required")]
[Display(Name = "Post")]
public string PostContent { get; set; }
[Required()]
[DataType(DataType.DateTime)]
public DateTime PostDate { get; set; }
//public int AuthorID { get; set; }
//public int CommentID { get; set; }
[NotMapped]
public virtual List<Comments> Comment { get; set; }
public virtual Users user { get; set; }
}
and this class has a many to one relationship with the users class below
public class Users
{
public Users()
{
}
[Key]
public int UserID { get; set; }
[Required()]
public string Firstname { get; set; }
[Required()]
public string Lastname { get; set; }
[Required()]
[DataType(DataType.Date, ErrorMessage="Enter a Valid Date")]
public DateTime DOB { get; set; }
//[Required()]
//public string Photo { get; set; }
[Required()]
public string Sex { get; set; }
[Required()]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
public DateTime RegDate { get; set; }
[Required()]
[Column("Username")]
//[Remote("doesUserNameExist", "Account", HttpMethod = "POST", ErrorMessage = "User name has already been taken. Please enter a different User name.")]
public string Username { get; set; }
[Required()]
[DataType(DataType.Password)]
public string Password { get; set; }
public string PasswordSalt { get; set; }
[System.ComponentModel.DataAnnotations.Compare("Password", ErrorMessage="Passwords do not match")]
[DataType(DataType.Password)]
//[NotMapped]
//public string ConfirmPassword { get; set; }
public virtual List<Posts> Post { get; set; }
public virtual List<Comments> Comment { get; set; }
}
The database table for the Posts.cs class has a field called user_UserID which i assume is to store the id of the user that creates a post. I'm trying to save the posts to the database using the below code
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Posts</legend>
<div class="form-group">
#Html.LabelFor(model => model.PostTitle)
#Html.TextBoxFor(model => model.PostTitle, new {#class = "form-control"})
#Html.ValidationMessageFor(model => model.PostTitle)
</div>
<div class="form-group">
#Html.LabelFor(model => model.PostContent)
#Html.TextAreaFor(model => model.PostContent, new {#class = "form-control"})
#Html.ValidationMessageFor(model => model.PostContent)
</div>
<input type="hidden" name="user.UserID" value="#Session["LoggedUserID"]" />
<div>
<input type="submit" value="Submit" />
</div>
</fieldset>
}
As you can see, the user ID i'm saving to the database table is gotten from the user ID stored in the Session["LoggedUserID"] variable. The controller that handles the saving is below
public class PostsController : Controller
{
//
// GET: /Posts/
BlogContext db = new BlogContext();
public ActionResult Index()
{
return View();
}
[HttpGet]
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Posts Post)
{
var errors = ModelState.Values.SelectMany(v => v.Errors);
try
{
if (ModelState.IsValid)
{
var newPost = db.Post.Create();
newPost.PostTitle = Post.PostTitle;
newPost.PostContent = Post.PostContent;
newPost.PostDate = DateTime.Now;
newPost.user.UserID = Post.user.UserID;
db.Post.Add(newPost);
db.SaveChanges();
return RedirectToAction("Index", "Posts");
}
else
{
ModelState.AddModelError("", "Something is wrong with the model");
}
}
catch (NullReferenceException ex)
{
Debug.WriteLine("Processor Usage" + ex.Message);
}
return View();
}
}
I attached a breakpoint to the Modelstate.IsValid line and then debugged, but i noticed the ModelState is always evaluating to false and the ModelState.AddModelError is showing that is there is something wrong with validating the model. Ive tried all possible tweakings all to no avail. I need help. How can i save the Posts to the database table without this problem.
Please what am i doing wrong?
I suspect ModelState is invalid because you are posting a value for user.UserId (the hidden input) which is initializing the property User which is invalid because of the validation attributes applied to other properties of User. It would be better to create a view model for creating new Posts that contain only the properties you need to display/edit. It is not necessary to include a property for User (the author of the post) since this can be set in the controller when you save the data (similar to what you are doing for the PostDate property
View model
public class PostVM
{
[Required(ErrorMessage="A Title is required for your Post")]
[Display(Name="Title")]
public string Title { get; set; }
[Required(ErrorMessage="This Field is Required")]
[Display(Name = "Post")]
public string Content { get; set; }
}
Controller
[HttpGet]
public ActionResult Create()
{
PostVM model = new PostVM();
return View(model);
}
[HttpPost]
public ActionResult Create(PostVM model)
{
if(!ModelState.IsValid)
{
return View(model);
}
// Initialize new Posts
// Map properties from the view model, set the user and date
// Save and redirect
}
View
#model PostVM
#using (Html.BeginForm()) {
....
<div class="form-group">
#Html.LabelFor(model => model.Title)
#Html.TextBoxFor(model => model.Title, new {#class = "form-control"})
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="form-group">
#Html.LabelFor(model => model.Content)
#Html.TextAreaFor(model => model.Content, new {#class = "form-control"})
#Html.ValidationMessageFor(model => model.Content)
</div>
<input type="submit" value="Submit" />
}

What is the name convention of binding a complex ViewModels?

I have these two ViewModels
public class AboutViewModel : ViewModel
{
public override long Id { get; set; }
public override string PageTitle { get; set; }
public override string TitleDescription { get; set; }
public override string ContentTitle { get; set; }
public virtual AboutItemViewModel AboutItem { get; set; }
}
public class AboutItemViewModel
{
public long Id { get; set; }
[AllowHtml]
public string Content { get; set; }
public string ImageUrl { get; set; }
public HttpPostedFileBase FileToUpload { get; set; }
}
Here is my controller:
[ValidateInput(false)]
[ValidateAntiForgeryToken, HttpPost]
public ActionResult Create(long? siteid, long? cid, AboutViewModel model)
{
return View(model);
}
Here is my View:
#using (Html.BeginForm("Create", "About", new { siteid = ViewData["siteid"], cid = ViewData["cid"] },FormMethod.Post,new { enctype = "multipart/form-data", #class = "form-horizontal rtl", autocomplete = "off" }))
{
<div class="controls">
<input type="file" name="FileToUpload" id="FileToUpload" style="margin-right: -9px;">
</div>
<div class="controls">
#Html.ValidationMessageFor(o => o.AboutItem.FileToUpload, "", new { id = "spanfile", #class = "alert alert-block alert-error span3 pull-right", style = "margin-right: 160px;" })
</div>
<div class="control-group pull-left">
<button type="submit" class="btn btn-large" data-toggle="button">Save</button>
</div>
}
How to bind the file to FileToUpload to stop returning me a null?
Except:
If I put it in the main AboutViewModel than it's returns a correct value.
Since the FileToUpload property is in the AboutItem proprety, which is a class property of the parent ViewModel, you need to preface the name of your input element with the property it came from. That's a long way of saying that the name of your file input should be AboutItem.FileToUpload.
<input type="file" name="AboutItem.FileToUpload" id="AboutItem_FileToUpload" />
This should take care of the model binding for you. Additionally, you can test this by using an HTML helper on on of the other properties of the AboutItem class. For instance:
#Html.TextBoxFor(x=>x.AboutItem.Id)
This should render in the HTML
<input type="text" name="AboutItem.Id" id="AboutItem_Id />
EDIT
Apparently the id attribute will be rendered with an underscore instead of a dot. However, since the ID attribute is not used in model binding, it shouldn't really matter.