I am attempting to display a form that allows a user to input a new assignment for a person. I'm using a DateTime.cshtml EditorTemplate to handle DateTime values for the assignment. The non-nullable DateTime works fine. The nullable DateTime causes an "InvalidOperationException: Nullable object must have a value."
I have a simple viewmodel that looks like this:
AssignmentViewModel.cs:
public Person Person { get; set; }
public Assignment NewAssignment { get; set; }
Assignment.cs contains:
public DateTime AssignmentStartDate { get; set; }
public DateTime? AssignmentEndDate { get; set; }
My AssignmentController Create() method looks like:
public ViewResult Create(int personId)
{
Person person = personRepository.GetPersonById(personId);
var newAssignment = new AssignmentViewModel { Person = person, NewAssignment = new Assignment() };
return View(newAssignment);
}
My Create.cshtml view looks like this:
#model AssignmentViewModel
#using (Html.BeginForm("Create", "Assignment"))
{
#Html.Hidden("NewAssignment.PersonId", Model.Person.PersonId)
#Html.LabelFor(x => x.NewAssignment.AssignmentStartDate):
#Html.EditorFor(x => x.NewAssignment.AssignmentStartDate.Date, new { cssClass = "datePicker" })
<br />
#Html.LabelFor(x => x.NewAssignment.AssignmentEndDate):
#Html.EditorFor(x => x.NewAssignment.AssignmentEndDate.Value.Date, new { cssClass = "datePicker" })
<br />
<input type="submit" value="Send />
}
My DateTime.cshtml EditorTemplate looks like:
#model DateTime?
#{
String modelValue = "";
if (Model.HasValue)
{
if (Model.Value != DateTime.MinValue)
{
modelValue = Model.Value.ToShortDateString();
}
}
}
#Html.TextBox("", modelValue, new { #class = "datePicker" })
When I attempt to load the Create view, I get the exception mentioned above on the line "#Html.EditorFor(x => x.NewAssignment.AssignmentEndDate.Value)".
You may be wondering why I'm passing in AssignmentEndDate.Value.Date instead of just passing in AssignmentEndDate; the reason is because I'm trying to get to the point where I'm splitting DateTime into Date and a TimeOfDay field and recombine them with a DateTimeModelBinder. I am using a similar technique to the one shown here and here.
I -can- bypass the error, by changing my controller Create() method to instantiate the ViewModel with AssignmentEndDate set to DateTime.MinValue, but this seems completely wrong for a nullable DateTime:
var newAssignment = new AssignmentViewModel
{
Person = person,
NewAssignment = new Assignment { AssignmentEndDate = DateTime.MinValue }
};
Something strange happens after I "bypass" the error by supplying a value for the nullable DateTime; the un-required nullable DateTime property (AssignmentEndDate.Date) fails client side validation. Trying to submit the form highlights the field in red.
How can I handle this correctly?
The problem is that you're trying to retrieve the AssignmentEndDate.Value.Date, but AssignmentEndDate is null, which results in this error.
Since your editor template accepts a DateTime?, you should just pass along the AssignmentEndDate. In other words, remove the .Value.Date from the view:
#Html.EditorFor(x => x.NewAssignment.AssignmentEndDate, new { cssClass = "datePicker" })
Since your editor template is using ToShortDateString(), there's no need to "truncate" the time from the date at all.
Update
Regarding your desire to have separate "Date" and "Time" editors:
You can do this 2 ways.
1 - Your current DateTime? editor renders a field for the Model.Value.Date, so you could simply extend this to also render a field for the Model.Value.TimeOfDay. Example:
#{
DateTime? modelDate = (Model == null) ? (DateTime?)null : Model.Value.Date;
TimeSpan? modelTime = (Model == null) ? (TimeSpan?)null : Model.Value.TimeOfDay;
}
#Html.TextBox(..., modelDate, new{#class="datePicker"})
#Html.TextBox(..., modelTime, new{#class="timePicker"})
2 - You could split the above functionality into 2 separate editors, "DateOnly" and "TimeOnly". Then, update your view to call both editors:
#Html.EditorFor(x => x.NewAssignment.AssignmentEndDate, "DateOnly")
#Html.EditorFor(x => x.NewAssignment.AssignmentEndDate, "TimeOnly")
The choice is up to you, and whether you want to keep the Date and Time parts separate or together, but this is how I'd go about solving this problem.
create a DateTime.cshtml in your Shared/DisplayTemplate folder
#model Nullable<DateTime>
#(Model != null ? string.Format(ViewData.ModelMetadata.DisplayFormatString ?? "{0:d}", Model) : string.Empty)
this supports metadata from datannotations to be used if found.
UPDATE: GetValueOrDefault treats it as a DateTime and therefore the required Field validators are getting attached, because the original expression is for a datetime not a nullable datetime.
Therefore the solution below doesn't work.
Like the asker, I also used the DateTimeModelBinder from here:
Here's the Link
This is how I solved a similar situation:
#Html.EditorFor(x => x.NewAssignment.AssignmentEndDate.GetValueOrDefault().Date)
And this is what my DateTime EditorTemplate looks like:
#model DateTime
#Html.TextBox("", Model != default(DateTime) ? Model.ToShortDateString() : String.Empty, new { #class = "datepicker", #maxlength = "10" })
Related
I have an ASP.NET MVC3 application. I have the following ViewModel:
public class MyViewModel
{
public string Year { get; set; }
public string Month {get; set; }
public IEnumerable<SelectListItem> Years
{
get
{
return Enumerable.Range(2000, DateTime.Now.Year - 2000).Select(x => new SelectListItem
{
Value = x.ToString(),
Text = x.ToString()
});
}
}
}
And the following View:
#model MyNamespace.MyViewModel
#Html.DropDownListFor(
x => x.Year,
new SelectList(Model.Years, "Value", "Text"),
"-- select year --"
)
#Html.DropDownListFor(
x => x.Month,
Enumerable.Empty<SelectListItem>(),
"-- select month --"
)
I fill the DropDownList for Month with a jQuery function that is triggered onchange from the DropDownList for Year, which works perfectly. When the form is posted to the server and then the view is rendered back I want to keep the values in the 'Month' DropDownList as it happens correctly for Year. Therefore I tried (besides the jQuery script):
public class MyViewModel
{
public string Year { get; set; }
public string Month {get; set; }
public IEnumerable<SelectListItem> Months
{
get
{
if(Year != null)
{
return Enumerable.Range(1, 12).Select(x => new SelectListItem
{
Value = x.ToString(),
Text = x.ToString()
});
}
}
}
And in the View:
#Html.DropDownListFor(
x => x.Month,
new SelectList(Model.Months, "Value", "Text"),
"-- select month --"
)
I left Years out of the code to make things shorter. This code throws a NullReferenceException at the first run because the IEnumerable is empty. how can I achieve my goal?
You have to repopulate the month list in your action.
The POST is stateless is not like asp.net webforms.
Just do something like this in your code:
public ViewResult MyAction(MyViewModel model)
{
if (ModelState.IsValid)
{
//.. do your stuff ...
//return redirect to index if everything went ok
}
//something went wrong return the model
model.Years = new SelectList(/* code to populate years */, "Value", "Text", Model.Year);
model.Months = new SelectList(/* code to populate months */, "Value", "Text", Model.Month);
}
As a side note I prefer to use SelectList instead of IEnumerable<SelectListItem> beacuse I can set the default value in the controller (a better fit for the controller responsibility in my opinion) and not in the View.
I'm working on the first MVC3 project at our company, and I've hit a block. No one can seem to figure out what's going on.
I have a complex Model that I'm using on the page:
public class SpaceModels : List<SpaceModel> {
public bool HideValidation { get; set; }
[Required(ErrorMessage=Utilities.EffectiveDate + Utilities.NotBlank)]
public DateTime EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
}
In the Controller, I create a SpaceModels object with blank SpaceModels for when Spaces get combined (this would be the destination Space).
// Need a list of the models for the View.
SpaceModels models = new SpaceModels();
models.EffectiveDate = DateTime.Now.Date;
models.DisplayEffectiveDate = true;
models.Add(new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
return View("CombineSpaces", models);
Then in the View, I am using that SpaceModels object as the Model, and in the form making a TextBox for the Effective Date:
#model Data.SpaceModels
#using (Html.BeginForm("CombineSpaces", "Space")) {
<div class="EditLine">
<span class="EditLabel LongText">
New Space Open Date
</span>
#Html.TextBoxFor(m => m.EffectiveDate, new {
size = "20",
#class = "datecontrol",
// Make this as a nullable DateTime for Display purposes so we don't start the Calendar at 1/1/0000.
#Value = Utilities.ToStringOrDefault(Model.EffectiveDate == DateTime.MinValue ? null : (DateTime?)Model.EffectiveDate, "MM/dd/yyyy", string.Empty)
})
#Html.ValidationMessageFor(m => m.EffectiveDate)
</div>
<hr />
Html.RenderPartial("_SpaceEntry", Model);
}
The Partial View that gets rendered iterates through all SpaceModels, and creates a containing the Edit fields for the individual SpaceModel objects. (I'm using the List to use the same Views for when the Spaces get Subdivided as well.)
Then on the HttpPost, the EffectiveDate is still back at it's DateTime.MinValue default:
[HttpPost]
public ActionResult CombineSpaces(SpaceModels model, long siteID, long storeID, DateTime? effectiveDate) {
// processing code
}
I added that DateTime? effectiveDate parameter to prove that the value when it gets changed does in fact come back. I even tried moving the rendering of the TextBox into the _SpaceEntry Partial View, but nothing worked there either.
I did also try using the #Html.EditorFor(m => m.EffectiveDate) in place of the #Html.TextBoxFor(), but that still returned DateTime.MinValue. (My boss doesn't like giving up the control of rendering using the #Html.EditorForModel by the way.)
There has to be something simple that I'm missing. Please let me know if you need anything else.
Looking at the source code for DefaultModelBinder, specifically BindComplexModel(), if it detects a collection type it will bind the individual elements but will not attempt to bind properties of the list object itself.
What model binding does is attempt to match the names of things or elements in the view to properties in your model or parameters in your action method. You do not have to pass all of those parameters, all you have to do is add them to your view model, then call TryUpdateModel in your action method. I am not sure what you are trying to do with SpaceModel or List but I do not see the need to inherit from the List. Im sure you have a good reason for doing it. Here is how I would do it.
The view model
public class SpacesViewModel
{
public DateTime? EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
public List<SpaceModel> SpaceModels { get; set; }
}
The GET action method
[ActionName("_SpaceEntry")]
public PartialViewResult SpaceEntry()
{
var spaceModels = new List<SpaceModel>();
spaceModels.Add(
new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
var spacesVm = new SpacesViewModel
{
EffectiveDate = DateTime.Now,
DisplayEffectiveDate = true,
SpaceModels = spaceModels
};
return PartialView("_SpaceEntry", spacesVm);
}
The POST action method
[HttpPost]
public ActionResult CombineSpaces()
{
var spacesVm = new SpacesViewModel();
// this forces model binding and calls ModelState.IsValid
// and returns true if the model is Valid
if (TryUpdateModel(spacesVm))
{
// process your data here
}
return RedirectToAction("Index", "Home");
}
And the view
<label>Effective date: </label>
#Html.TextBox("EffectiveDate", Model.EffectiveDate.HasValue ?
Model.EffectiveDate.Value.ToString("MM/dd/yyyy") : string.empty,
new { #class = "datecontrol" })
Sometimes you need to explicitly bind form data using hidden fields such as
#Html.HiddenField("EffectiveDate", Model.EfectiveDate.)
In order to bind the properties of the SpaceModel object you can add individual properties such as SiteID to the view model or add a SpaceModel property for a single SpaceModel. If you want to successfully bind a complex model, add it as a Dictionary populated with key-value pairs rather than a List. You should then add the dictionary to the view model. You can even add a dictionary of dictionaries for hierarchical data.
I hope this helps :)
I feel like I'm taking crazy pills. I have a dropdownlist for a view that reads from our database all of the wine producers we have. I want to set the selectedValue to a particular ID driven by the referring page. I can see it picks up the selectedValue in debug, I see the selected value populated (906 for this example), but it doesn't set the dropdownlist to the correct value when the page is rendered, it always defaults to 1 for the default value. I've tried creating the selectList in razor as opposed to my controller, but nothing works. Any help on this would be appreciated, I'm guessing it is something small.
Controller:
if (User.IsInRole("admin"))
{
if (ID != 0)
{
ViewBag.ProducerSelect = new SelectList(db.Producers.OrderBy(p => p.Name), "ProducerID", "Name", ID);
}
else
{
ViewBag.ProducerSelect = new SelectList(db.Producers.OrderBy(p => p.Name), "ProducerID", "Name");
}
}
View:
if (User.IsInRole("producereditor"))
{
<h3>#ViewBag.ProducerName</h3>
}
else
{
<div class="editor-label">
#Html.LabelFor(m => m.Wine.ProducerID, "Producer")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.Wine.ProducerID, ViewBag.ProducerSelect as SelectList)
</div>
}
Tried the below but no success:
ViewBag.ProducerSelect = new SelectList(from p in db.Producers
orderby p.Name
select new { p.ProducerID, p.Name }
, "ProducerID", "Name", ID);
If you want to preselect an item, You set that value to your ProducerId property.
var yourViewModelObj=new YourViewModel;
yourViewModelObj.Wine.ProducerId=906; //or whatever value you want
return View(yourViewModelObj);
Suggestion : For better code readablity/Maintenance, Try to avoid ViewBag / ViewData and use a ViewModel to pass the data.
I would add a Property to my ViewModel to hold the Collection of Producers
public class WineViewModel
{
//Other Existing properties also
public IEnumerable<SelectListItem> Producers{ get; set; }
public string SelectedProducer { get; set; }
}
Then in yout GetAction method, you can set the value like this, If you want to set one select option as the default selected one.
public ActionResult CreateWine()
{
var vm=new WineViewModel();
//The below code is hardcoded for demo. you mat replace with DB data.
vm.Producers= new[]
{
new SelectListItem { Value = "1", Text = "Prodcer A" },
new SelectListItem { Value = "2", Text = "Prodcer B" },
new SelectListItem { Value = "3", Text = "Prodcer C" }
};
//Now let's set the default one's value
vm.SelectedProducer = "2";
return View(vm);
}
And in your Strongly typed View,
#Html.DropDownListFor(x => x.SelectedProducer,
new SelectList(Model.Producers, "Value", "Text"), "Select Producer")
The HTML Markup generated by above code will have the HTML select with the option with value 2 as selected one.
I figured this out. I had ViewModel.wine.ProducerID elsewhere on the page in a hidden field, and that defaults to 1, so I just assigned that to passed in value, and it worked great. I knew it was something like that. Thanks!
User a ViewModel ex WineViewModel
public class WineViewModel
{
public Wine Wine { get; set; }
public SelectList PProducerList { get; set; }
public WineViewModel() { }
public WineViewModel(Wine wine)
{
this.Wine = wine;
}
}
Try the following in your controller
var model = new WineViewModel( selectwine);
model.ProjectTypeList = new SelectList( from p in db.Producers
orderby p.Name
select new { p.ID, p.Name }, "ID", "Name")
notice how I am exclusively declaring which is the ID and which is the Value in my SelectList
Then in your view do
#Html.DropDownListFor(model => model.Wine.ProducerID, Model.ProjectTypeList)
I am working with razor view engine in asp.net mvc3.
Now, I need an input for a DateTime which should display the value in a fixed format (say dd-MMM-yyyy). So I can do:
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd-MMM-yyyy}")]
public DateTime StartDate { get; set; }
And in the view:
#Html.EditorFor(model => model.StartDate)
But I need to add a class in the input. Which I think is not possible in EditorFor.
So I could use
#Html.TextBoxFor(model => model.StartDate, new { #class = "Date" })
But the display format does not work in this case.
Model can be null. So,
#Html.TextBox("StartDate", string.Format("{0:dd-MMM-yyyy}", Model.StartDate))
will throw NullReferenceException.
ophelia's way is a quicker version of mine. But this way is useful if you're going to have a few date fields in your application. So this is the way i like to do it:
-> In your solution explorer, create a folder inside Shared called "EditorTemplates"
-> In there, add a new (not strongly typed) Partial View. Call it "DateTime".
-> Open this view, and remove any code in there, and throw the following code in there:
#model System.DateTime
#Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { #class = "date" /* + any other html attributes you want */ })
-> In your ViewModel, your date field should be like this:
[Display(Name = "My Date:"), DisplayFormat(DataFormatString = "{0:dd-MMM-yyyy}", ApplyFormatInEditMode = true)]
public DateTime MyDate { get; set; }
-> In your main view, and any view you want to use a date field, you can just use EditorFor on the property, like so:
#Html.EditorFor(m => m.MyDate)
You can initialize the date field in your controller or set a default date in your viewmodel's constructor.
I use this and its works fine:
#Html.TextBox("ExpiryDate", String.Format("{0:ddd, dd MMM yyyy}", DateTime.Now), new { id = "expirydate" })
Hope this is what you mean/need :)
I have Results page that contains an signup form. I'm trying to use client-side validation on the email model, but I can't because the model type is different than the type I'm posting.
class Results
{
... // some results data to display
}
class EmailSignup
{
public virtual int Id { get; set; }
[Required(ErrorMessage = "Please enter your email.")]
[DataType(DataType.EmailAddress)]
[RegularExpression(#"^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$", ErrorMessage = "Please enter a valid email address.")]
public virtual string Email { get; set; }
}
Results.cshtml
#model MyApp.Results
[display results]
...
#using (Html.BeginForm("SubmitEmail", "AnalysisResults", FormMethod.Post))
{
<fieldset>
#Html.TextBoxFor( model => model.???? )
</fieldset>
}
The type I want the user to submit is EmailSignup, not Results.
Move the form to a partial view that takes an EmailSignup model.
This can be done quite easily. You just have to do it like this:
var contactModel = new ContactModel();
#Html.TextBoxFor(m => contactModel.Title)
#Html.ValidationMessageFor(m => contactModel.Title)
The validation works like a charm.
I have find out 2 more ways
Override the Name attribute for TextBoxFor and set it as the property name.
var formModel = new ForgotPasswordFormModel();
#Html.TextBoxFor(m => formModel.UsernameOrEmail, new { Name = "UsernameOrEmail" })
Specify the same exact model name as the post method parameter.
var formModel = new ForgotPasswordFormModel();
#using (Html.BeginForm("ChangePassword", "LoginSurface")
{
#Html.TextBoxFor(m => formModel.UsernameOrEmail)
}
...
public virtual ActionResult ChangePassword(ForgotPasswordFormModel formModel)
You could create another HtmlHelper like this
var emailSignupHtml = new HtmlHelper<EmailSignup>(Html.ViewContext, new ViewDataContainer<EmailSignup>(new EmailSignup()));
and use it like this
#emailSignupHtml.TextBoxFor(m => m.Email)
For the ViewDataContainer I use following helper class
public class ViewDataContainer<TModel> : ViewDataDictionary<TModel>, IViewDataContainer
{
public ViewDataContainer(TModel model) : base (model)
{
ViewData = new ViewDataDictionary(model);
}
public ViewDataDictionary ViewData { get; set; }
}
I guess you can also try #HTML.Action("EmaialSignup")
and your controller will have a Function calling the partial view
if you have to render multiple model bounded View in this view