Time-only editor template and validation - c#

Having some trouble creating a time-only input field with unobtrusive validation, mapping to a non-nullable DateTime field.
I am working with a database-first approach. The database has datetime fields used to store both dates and times. By that I mean some datetimes have real date data and 00:00:00 for time while others have meaningless date data with real times. I understand this came about due to limitations with EF5 and C# datatypes. Now using EF6, but not particularly intending to change the database.
Here is the view model
[UIHint("Date")]
public DateTime MatchDate { get; set; }
[UIHint("Time")]
public DateTime StartTime { get; set; }
The date-only field, MatchDate, is working. It uses an EditorFor template, Date.cshtml (below). It allows text input, correctly attaches a jQuery datepicker and correctly validates server side and client-side with unobtrusive.
#model DateTime
#if (Model == DateTime.MinValue)
{
#Html.TextBox("", "", new { #class = "datepicker" })
}
else
{
#Html.TextBox("", String.Format("{0:d/M/yyyy}", Model), new { #class = "datepicker" })
}
So I created a new EditorFor template for Time.cshtml
#model DateTime
#if (Model == DateTime.MinValue)
{
#Html.TextBox("", "", new { #class = "timepicker" })
}
else
{
#Html.TextBox("", String.Format("{0:h\\:mm tt}", Model), new { #class = "timepicker" })
}
At this point the Html input element gets an unwanted data-val-date attribute, causing unobtrusive date validation. I found that by adding [DataType(DataType.Time)], the data-val-date is no longer added. While this is a good thing, I'm not if that is exactly (and only) what the datatype attribute is supposed to do. So this is the first part of the question.
Next, I have created custom validation rules (server & client-side) for a 12-hr time format.
Annotation:
public class TimeTwelveAttribute : ValidationAttribute, IClientValidatable
{
private const string _ErrorMessage = "Invalid time format. Please use h:mm am/pm (TimeAttribute)";
public TimeTwelveAttribute()
{
this.ErrorMessage = _ErrorMessage;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = _ErrorMessage,
ValidationType = "timetwelve"
};
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
DateTime time;
if (value == null || !DateTime.TryParse(value.ToString(), out time))
{
return new ValidationResult(_ErrorMessage);
}
return ValidationResult.Success;
}
}
javascript
$.validator.unobtrusive.adapters.addSingleVal("timetwelve");
$.validator.addMethod(
"timetwelve",
function (value, element) {
return this.optional(element) || /^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(value);
},
"Invalid time format. Please use h:mm am/pm"
);
However, adding the [TimeTwelve] data annotation to StartTime has no effect:
Server-side validation is occurring, but it's creating a different error message that I can't override: "The value 'asdf' is not valid for Start Time."
There is no client-side validation message popping up if I type 'asdf' in to the time field.
I gather this is caused by the postback values being mapped in to C# DateTimes. Whatever mechanism does that takes a higher priority and overrides all other validation. As an exercise, I rewrote StartTime as a string (and updated the templates etc.) and sure enough [TimeTwelve] works, client-side and server-side as expected. I feel like this is aspect has not been covered in any of the related questions here on stackoverflow.
So the second part of my question, given that I would really prefer to use DateTimes in my view model, how can I get validation to work?
This is the closest question I could find, but is from 2013.

First, to get your client side validation working, change your scripts to
$.validator.unobtrusive.adapters.add('timetwelve', function (options) {
options.rules['timetwelve'] = {};
options.messages['timetwelve'] = options.message;
});
// no need to hard code the message - it will use the one you have defined in the validation atribute
$.validator.addMethod('timetwelve', function (value, element) {
return this.optional(element) || /^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(value);
});
or you can use the .addBool() method
$.validator.unobtrusive.adapters.addBool("timetwelve");
As far as for receiving the The value 'asdf' is not valid for Start Time if you your enter "asdf" in the textbox, that is the default behavior. The DefaultModelBinder uses ValueConverters to parse the values in the Request to the property type. Since asdf cannot be converted to DateTime, a ModelState error is added and no further validation is done (because its already invalid and there would be no point). Of course, once client side validation is working, you will never get to that point anyway (unless the user has disabled javascript or its a malicious user).
Note also your IsValid() method can simply be
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
By the time you reach here, the DefaultModelBinder has already set the DateTime value (by combining today's date with the time entered in the form), so there is no need to try and parse it again.

Related

conversion of a datetime2 data type to a datetime data type exception with dates after 1753/1/1

I have added a property to my Customer Model, created a migration and updated the database. The field created in DB is datetime.
here is my Model code
public DateTime BirthDate { get; set; }
here is the view that is actually a form where I insert date and other fields and submit the form.
<div class="form-group">
#Html.LabelFor(m => m.Customer.BirthDate)
#Html.TextBoxFor(m => m.Customer.BirthDate, "{0: dd-MM-yyyy}", new { #class = "form-control" })
</div>
and here is the Action in the controller
public ActionResult Save(Customer customer)
{
if (customer.Id == 0)
{
_context.Customers.Add(customer);
}
else
{
var customerInDb = _context.Customers.Single(c => c.Id == customer.Id);
customerInDb = customer;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customers");
}
I have papulated the BirthDate field with 23/04/1976 and debug the application and check the value of customer.BirthDate which is 23/04/1976 12:00:00 AM. The date is note before 1753/1/1 but I m receiving the said exception.
I have also tried the following:
Make the field nullable by
public DateTime? BirthDate { get; set; }
the exception is gone but the date is not being saved to the database.
Removed the formate "{0: dd-MM-yyy}" from the view but in vain.
Inserted dates in different formats e.g. 23-Apr-1952, 1985-12-01 and 1987/1/2 but didn't work.
Visual Studio 2013 and Database is LocalDb of visual studio.
The fact that the error goes away by making the property nullable gives the answer. If a value was being set, then you would get exactly the same error as when the field was not nullable.
Therefore, the value is not being set, which stores a null if the field is nullable (confirmed by you saying it doesn't store the value - it's storing null).
So for a not-nullable property, when it isn't set, it will have the default value for a dot net DateTime, which is DateTime.MinValue, which would be valid if stored in an SQL server datetime2 but not in a datetime, where you get the 1753 error.
By the way, I'm assuming this LocalDb is basically SQL server, based on the 1753 error. For the record, in SQL server you should be using datetime2: DateTime2 vs DateTime in SQL Server
But this field is actually a Date, so you should be using the date SQL type.
So in summary, I suspect the BirthDate value in the Customer passed to the Save method has the value DateTime.MinValue. Now you need to work out why... (but that's a different question)
P.S. Are you using dd-MM-yyy on purpose? Should there be an extra y?
The actual problem lies in both DateTime viewmodel property and this TextBoxFor helper:
#Html.TextBoxFor(m => m.Customer.BirthDate, "{0: dd-MM-yyy}", new { #class = "form-control" })
Depending on current culture setting, this will generate plain input with specified date format which may not matched with current culture setting in server, which causing default model binder to ignore its value and uses DateTime.MinValue as default value (because it's not a nullable type), which doesn't fit for datetime column type in SQL that has minimum value of 1753-01-01 (the equivalent type of System.DateTime in T-SQL is datetime2).
The most recommended setup to create input helper for DateTime property is using DataTypeAttribute set to DataType.Date and DisplayFormatAttribute to specify the format string:
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
public DateTime BirthDate { get; set; }
Then, use either EditorFor or TextBoxFor with type="date" attribute to generate date input:
#* TextBoxFor *#
#Html.TextBoxFor(model => model.Customer.BirthDate, new { #class = "form-control", type = "date" })
#* EditorFor *#
#Html.EditorFor(model => model.Customer.BirthDate, new { htmlAttributes = new { #class = "form-control" } })
Note: The same setup like above also apply for Nullable<DateTime>/DateTime? property.
Additionally you should check for IsValid property of ModelState before using SaveChanges() and return the same view when validation failed:
[HttpPost]
public ActionResult Save(Customer customer)
{
if (ModelState.IsValid)
{
if (customer.Id == 0)
{
_context.Customers.Add(customer);
}
else
{
var customerInDb = _context.Customers.Single(c => c.Id == customer.Id);
customerInDb = customer;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customers");
}
else
{
// validation failed, show the form again with validation errors
return View(customer);
}
}
Related issue: ASP.NET MVC date not saving when posting

C# MVC TextBoxFor Currency Prefix

Is it possible with either Bootstrap, HTML5 or jQuery to have a currency prefix within the input field as a display element and not part of the actual input value?
For example I wish to display £550.00 in the Amount field, but the data as 550.00
I have tried using w2ui, jQuery Price Format and jQuery maskMoney, all 3 do the same job basically, they can indeed prefix or suffix, but they don't actually store a value in the input, so posting data returns a null value.
Model.cs
[DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
public double Amount { get; set; }
HTML
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-addon">Amount</span>
#Html.TextBoxFor(m => m.Amount, new { #class = "form-control", #Value = #ViewBag.Amount, placeholder = "Amount", #Id = "Amount" })
</div>
</div>
// Dynamically setting value of field
$('#Amount').val(data.Amount);
// w2ui example
$('#Amount').val(data.Amount).w2field('money', { moneySymbol: '£' });
// jQuery Price Format example
$('#Amount').val(data.Amount).priceFormat({
prefix: '£'
});
I realise I can use another field to store the numeric value, but I have quite a few fields I need to display which are initially dynamically populated, but can be overridden by the user. So duplicating fields, one for display input and one hidden to store inputted data seems over kill.
Any assistance would be much appreciated :-)
I recommend wrapping your input with a span:
<span class="currency">£ #Html.TextBoxFor()</span>
And modifying that with css to make it format nicely for yourself. Such as this.
Or just add it as a label:
<label for="currencyInput">Amount in £</label>
Otherwise, you'll be stuck trying to remove / add the prefix everywhere. Keep your input as the actual value.
The issue wasn't with any of the jQuery libraries but the fact that the form was posting a string, whereas the model was expecting a double.
So I have implemented a ModelBinder to allow the post of string data containing the '£' pound sign and convert to a double.
MyModelBinder.cs
namespace WebApplication1.Models
{
public class MyModelBinder : DefaultModelBinder
{
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
if (propertyDescriptor.ComponentType == typeof(MyModel))
{
if (propertyDescriptor.Name == "Amount")
{
var obj = bindingContext.ValueProvider.GetValue("Amount");
return Convert.ToDouble(obj.AttemptedValue.Replace("£", ""));
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
}
HomeController.cs
[HttpPost]
public ActionResult Index([ModelBinder(typeof(MyModelBinder))]MyModel model)
HTML
$('#Amount').val(data.Amount).priceFormat({
prefix: '£'
});
I hope this proves useful to others.

SQL Server 2012 Date datatype contains 12:00:00AM

I have a MVC CRUD site that has a Date Approved Textbox. When the new entry is created it is stored properly as "dd-MM-yyyy" in the SQL Sever. When I want to update an entry the Date Required textbox contains the date AND 12:00:00AM . I'm guessing this is because SQL Server is a date datatype and .Net is a DateTime. Any idea how to get rid of the 12:00:00AM ?
Failed attempts...
I've tried adding it to my viewmodel
[DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
[Required(ErrorMessage="Required")]
[DataType(DataType.Date)]
[Display(Name="Date Requested")]
public DateTime? Date_Requested { get; set; }
and also...
string test = Convert.ToDateTime(model.Date_Requested).ToString;
EDIT
the html textbox
#Html.TextBoxFor(model => model.Date_Requested,new {#class="datepicker" })
EDIT JQuery DatePicker
$(".datepicker").datepicker({
onSelect: function () {
$(this).valid();
}
});
you could add another property, using ToShortDateString:
public string DateRequestedShortDate
{
get
{
return Date_Requested == null ? "" : Date_Requested.ToShortDateString();
}
}
or simply set the textbox value to the ShortDateString to keep your binding
TextBoxFor does not honor format attributes (not sure if this is intended or a bug). You can either use EditorFor:
#Html.EditorFor(model => model.Date_Requested,new {#class="datepicker" })
or set the format in TextBoxFor:
#Html.TextBoxFor(model => model.Date_Requested,
new {
#class="datepicker",
#Value = Model.Date_Requested.ToString("MM-dd-yyyy")
});
Expanding on Jonesy answer from above.
public string DateRequestedShortDate
{
get
{
return Date_Requested == null ? "" : Date.Parse(Date_Requested).ToShortDateString();
}
}
Parse won't mess up the data being inserted into SQL , hope it helps.

MVC&EF: remove time from datetime

I have a model that contains a datetime field.
The column in the DB which it prepresents is of datatype 'date', so it has no time value.
The model date field is bound to a jquery-ui datepicker in my view.
When the page loads, it has time value: 1989/02/14 12:00:00 AM
How can I prevent the time value from being added?
Do I have to manually strip out the time portion with jQuery for every date field?
EDIT:
There is no point in editing the model, when the page loads its still there
Controller:
ClientModel c = DBContext.Clients.find(id);
//Doing any kind of date formatting here to c.DateOfBirth is ignored
return PartialView("_ClientDetailsView", c);
View:
#Html.TextBoxFor(model => model.DateOfBirth , new { #class = "date-field" })
I'm thinking that the solution would be something like a model attribute or a HtmlHelper parameter.
You can use the DataType attribute for this. Decorate your DateOfBirth property in the ClientModel with it:
public class ClientModel
{
[DataType(DataType.Date)]
public DateTime DateOfBirth { get; set; }
}
Also see the DataType enum.
You can also use the DisplayFormat attribute if you the DataType attribute doesn't fit your needs:
[DisplayFormat(DataFormatString = "dd MM, yyyy")]
public DateTime DateOfBirth { get; set; }
Thanks for the help Henk
In the end I grew tired of trying to find a 'proper' solution so I wrote the following jquery function and calling it on each page as required.
function DropTimeSegments() {
$(".datepicker").each(function (index, item) {
$(item).val(FormatDate(new Date($(item).val())));
});
function FormatDate(Date) {
return Date.getFullYear() + '-' + pad(Date.getMonth(), 2) + '-' + pad(Date.getDate(), 2);
}
function pad(num, size) {
var s = num + "";
while (s.length < size) s = "0" + s;
return s;
}

Custom dataannotations and clientside validation MVC2

I have a property on my view model that is a custom class with a value property.
e.g.
class mycustomobj
{
public int? Value {get; set; }
}
public class myviewmodel
{
[DefaultablePercentRange]
public property mycustomobj { get; set; }
}
I have a custom range attribute DefaultablePercentRange that I decorate this property with so that I can check apprpiate inputs. Associated with this is the relevant javascript validator for clientside.
The javascript is:
Sys.Mvc.ValidatorRegistry.validators["defaultablePercentRange"] = function (rule) {
var _minimum = rule.ValidationParameters["minimum"];
var _maximum = rule.ValidationParameters["maximum"];
return function (value, context) {
if (!value || !value.length) {
return true; // return true as null values allowed
}
var n = Number.parseLocale(value);
return (!isNaN(n) && _minimum <= n && n <= _maximum);
};
}
I am also using Html.EditorFor on my view with templates so that I can output the property as mycustomobj.Value rather than just mycustomobj . So the view property in html ends up being rendered something like:
<input class="defaultable tiny" default="0" defaultwhen="0" id="mycustomobj_Value" name="mycustomobj.Value" type="text" value="" placeholder="0" style="">
Now my problem is the javascript validation is passing null into my clientside validators function. After a fair amount of investigation I have identified this being because the JSON created for my custom DataAnnotationsModelValidator is not using the full id of the property. For example the JSON created is:
{"FieldName":"mycustomobj","ReplaceValidationMessageContents":true,"ValidationMessageId":"mycustomobj_validationMessage","ValidationRules":[{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"},{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"}]}
Where I need:
{"FieldName":"mycustomobj.value","ReplaceValidationMessageContents":true,"ValidationMessageId":"mycustomobj_value_validationMessage","ValidationRules":[{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"},{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"}]}
My question is. How can I get the right property name serialized out for the clientside validation so that my clientside validation will work. My serverside works just fine.
Please let me know if anyone needs more info.
I ended up getting around this by using a combination of factors.
Created a model binder specifically for mycustomobj that knows how to set the value on mycustomobj
changed template so that .Value was output but the control name was still just property name
This meant that when binding back to the viewmodel on post I can ensure the correct property on mycustomobj was set. And in the javascript the javascript client validation code was being called appropiately as the correct input id was being set.

Categories