MVC 4 version of NerdDinner has binding issue when I create a dinner - asp.net-mvc

If you goto
http://nerddinner.codeplex.com/SourceControl/changeset/view/b1a032d1532b
Get the MVC4 version. Run it. Host a dinner. Click Create I always get this error (screenshot)
It is one of the main reference apps I want to use to help me learn so if anyone know what I am missing please tell me
I don't know if the screenshot worked but the error is :
IndexOutOfRangeException in this piece of code on the LatLongStr
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != null)
{
string[] latLongStr = valueProviderResult.AttemptedValue.Split(',');
string point = string.Format("POINT ({0} {1})", latLongStr[1], latLongStr[0]);
//4326 format puts LONGITUDE first then LATITUDE
DbGeography result = DbGeography.FromText(point, 4326);
return result;
}
return null;
}

That's probably because you're entering a value as the LatLongString without a , character
This command
string[] latLongStr = valueProviderResult.AttemptedValue.Split(',');
splits the given string if there is a , sign, to create the lat long.
Later, there is a reference to the two values
... latLongStr[1], latLongStr[0]);
However, if the value entered does not contain a , latLongStr only has 1 item, so latLongStr[1] is out of bounds.
You can prevent this by making sure a valid LatLong is entered.

Related

NodaTime OffsetDateTime Binding in ASP.NET MVC

I'm currently using NodaTime OffsetDateTimes in ASP.NET MVC to represent datetimes that I receive from an API.
In the front-end HTML I have a date picker control, which represents dates in the format dd/MM/yyyy.
Once the form containing the date in this format is posted to the server, the OffsetDateTime properties are null.
I've tried modifying the value prior to post, so that it matches the Rfc3339Pattern, but it still reaches the controller as null.
I've created the following custom Model Binder to make this work, and also cater for other instances where dates are passed in the Rfc3339Pattern:
public class OffsetDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
// try Rfc3339Pattern format
var parseResult = OffsetDateTimePattern.Rfc3339Pattern.Parse(valueResult.AttemptedValue);
if (parseResult.Success)
{
return parseResult.Value;
}
// try dd/MM/yyyy format
parseResult = OffsetDateTimePattern.Create("dd/MM/yyyy", CultureInfo.CurrentCulture,
OffsetDateTime.FromDateTimeOffset(DateTimeOffset.MinValue)).Parse(valueResult.AttemptedValue);
if (parseResult.Success)
{
return parseResult.Value;
}
return null;
}
}
Is it necessary to create a custom ModelBinder for this, or is there something else I can do, similar to the NodaTime JSON.NET seralizer config?

MVC3 IModelBinder and updating member data

I have a class like this
public class Position
{
public string Title { get; set; }
public IEnumerable<string> PhoneNumbers { get; set; }
}
I wanted to use a textarea to accept phone numbers as one per line. After that the model obviously doesn't bind correctly, so I found IModelBinder that can help with this, but I don't see how I can inject the transformed data back into the model.
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var phones = bindingContext.ValueProvider.GetValue("phones");
var values = phones.AttemptedValue;
var phoneList = ..... //split and stuff
//now what? how to set it back?
Thank you
Create a new class PhoneNumberList and use that instead of IEnumerable<string> in your model. Then you can create a custom model binder for PhoneNumberList by copying the existing code. Binding your existing model type Position will be otherwise unaffected.
Alternatively (but a more complex solution) - you can preserve the existing definition of IEnumerable<string>. You would subclass the DefaultModelBinder and examine the PropertyMetadata of any IEnumerable<string> property. If it contains some identifying metadata that you have added (e.g. via an attribute) then it could perform your custom binding, otherwise it reverts to the base binding.
As an aside - you could also consider using bindingContext.ModelName instead of hard-coding the value provider key to "phones":
var phones = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
Create a new instance of your model, populate its properties and return it. Thats it.
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var phones = bindingContext.ValueProvider.GetValue("phones");
var values = phones.AttemptedValue;
var phoneList = ..... //split and stuff
//IE
Position position = new Postion();
position.PhoneNumbers = phoneList;
return position;
}

asp.net mvc controller arguments a?b1,c2... how to parse variable number?

If a URL, in asp.net mvc, has args that are not catered for in the index/show method signature of a controller, is it possible to use something like a params string[] args in the signature to gather them up. Or what is a good way to do this when the arg is a list of separated values (ie not name/value pairs)?
We have users, ultimately, creating urls with a variable number of arguments & need to parse them.
This is the code we have at the moment, but we can't help thinking there's a better way without splitting the string ourselves:
var url = Request.RawUrl.Split('?');
if (url.Length > 1)
{
var queryString = url[1];
var queryStringArgs = queryString.Split('&');
var queryStringMembers = from arg in queryStringArgs
let c = arg.Split('=').Length == 1
where c
select arg;
ViewBag.QueryStringMembers = queryStringMembers.ToJson();
}
*Append: these args dont have name=value, it's just a list of values.
Request.QueryString doesn't seem to help us, as it treats these query string args differently because they are not name=value, they are just value. So it puts them in a Request.QueryString[null] key as comma separated
First things first what you have is a malformed URL. So you are totally on your own parsing it. You may also expect it to fail any time. Everything that is part of the querystring, i.e. following the first ?, must be url encoded, you should not have multiple ?.
This being said you could write a custom model binder:
public class CustomModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var tokens = controllerContext.HttpContext.Request.RawUrl.Split('?');
if (tokens.Length > 1)
{
return tokens.Skip(1).ToArray();
}
return null;
}
}
and then:
public ActionResult Index([ModelBinder(typeof(CustomModelBinder))]string[] args)
{
return View();
}
Request.QueryString.AllKeys will contain a string array of the argument names sent in on the QueryString. Is that what you're looking for?

Extending Sanderson's custom mvc ModelBinder for an object stored in session

In his wonderful MVC book Steven Sanderson gives an example of a custom model binder that sets and retrieves a session variable, hiding the data storage element from the controller.
I'm trying to extend this to cater for a pretty common scenario: I'm storing a User object in the session and making this available to every action method as a parameter. Sanderson's class worked ok when the User details weren't changing, but now i need to let the user edit their details and save the amended object back to the session.
My problem is that I can't work out how to distinguish a GET from a POST other than by checking the number of keys in bindingContext.ValueProvider.Keys, and this seems so wrong I'm sure I'm misunderstanding something.
Can anyone point me in the right direction? Basically all Actions need access to the current user, and the UpdateMyDetails action needs to update that same object, all backed by the Session. Here's my code...
public class CurrentUserModelBinder : IModelBinder
{
private const string userSessionKey = "_currentuser";
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var user = controllerContext.HttpContext.Session[userSessionKey];
if (user == null)
throw new NullReferenceException("The CurrentUser was requested from the CurrentUserModelBinder but no IUser was present in the Session.");
var currentUser = (CCL.IUser)user;
if (bindingContext.ValueProvider.Keys.Count > 3)
{
var firstName = GetValue<string>(bindingContext, "FirstName");
if (string.IsNullOrEmpty(firstName))
bindingContext.ModelState.AddModelError("FirstName", "Please tell us your first name.");
else
currentUser.FirstName = firstName;
var lastName = GetValue<string>(bindingContext, "LastName");
if (string.IsNullOrEmpty(lastName))
bindingContext.ModelState.AddModelError("LastName", "Please tell us your last name.");
else
currentUser.LastName = lastName;
if (bindingContext.ModelState.IsValid)
controllerContext.HttpContext.Session[userSessionKey] = currentUser;
}
return currentUser;
}
private T GetValue<T>(ModelBindingContext bindingContext, string key)
{
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(key, out valueResult);
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T));
}
}
Try inheriting from DefaultModelBinder instead of IModelBinder, then you can call base.BindModel to populate bindingContext.Model for mvc 1.0 or bindingContext.ModelMetadata.Model for mvc 2.0
To trigger bindingContext.Model to populate, call UpdateModel on the controller.
You need to add the statement from the book back in
if(bindingContext.Model != null)
throw new InvalidOperationException("Cannot update instances");
but change it to populate model and save on the session.
if(bindingContext.Model != null)
{
base.BindModel(controllerContext, bindingContext);
//save bindingContext.Model to session, overwriting current.
return bindingContext.Model
}

How to set decimal separators in ASP.NET MVC controllers?

I'm working with the NerdDinner application trying to teach myself ASP.NET MVC. However, I have stumbled upon a problem with globalization, where my server presents floating point numbers with a comma as the decimal separator, but Virtual Earth map requires them with dots, which causes some problems.
I have already solved the issue with the mapping JavaScript in my views, but if I now try to post an edited dinner entry with dots as decimal separators the controller fails (throwing InvalidOperationException) when updating the model (in the UpdateModel() metod). I feel like I must set the proper culture somewhere in the controller as well, I tried it in OnActionExecuting() but that didn't help.
I have just revisited the issue in a real project and finally found a working solution. Proper solution is to have a custom model binder for the type decimal (and decimal? if you're using them):
using System.Globalization;
using System.Web.Mvc;
public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
// Don't do this here!
// It might do bindingContext.ModelState.AddModelError
// and there is no RemoveModelError!
//
// result = base.BindModel(controllerContext, bindingContext);
string modelName = bindingContext.ModelName;
string attemptedValue = bindingContext.ValueProvider.GetValue(modelName)?.AttemptedValue;
// in decimal? binding attemptedValue can be Null
if (attemptedValue != null)
{
// Depending on CultureInfo, the NumberDecimalSeparator can be "," or "."
// Both "." and "," should be accepted, but aren't.
string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
string alternateSeperator = (wantedSeperator == "," ? "." : ",");
if (attemptedValue.IndexOf(wantedSeperator, StringComparison.Ordinal) == -1
&& attemptedValue.IndexOf(alternateSeperator, StringComparison.Ordinal) != -1)
{
attemptedValue = attemptedValue.Replace(alternateSeperator, wantedSeperator);
}
try
{
if (bindingContext.ModelMetadata.IsNullableValueType && string.IsNullOrWhiteSpace(attemptedValue))
{
return null;
}
result = decimal.Parse(attemptedValue, NumberStyles.Any);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
return result;
}
}
Then in Global.asax.cs in Application_Start():
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
Note that code is not mine, I actually found it at Kristof Neirynck's blog here. I just edited a few lines and am adding the binder for a specific data type, not replacing the default binder.
Set this in your web.config
<system.web>
<globalization uiCulture="en" culture="en-US" />
You appear to be using a server that is setup with a language that uses comma's instead of decimal places. You can adjust the culture to one that uses the comma's in a way that your application is designed, such as en-US.
Can you parse the text using the invariant culture - sorry, I don't have the NerdDinner code in fornt of me, but if you are passing in dot-separated decimals than the parsing should be OK if you tell it to use the invariant culture. E.g.
float i = float.Parse("0.1", CultureInfo.InvariantCulture);
Edit. I suspect that this is a bug in the NerdDinner code by the way, along the same lines as your previous problem.
I have a different take on this, you might like it. What I don't like about the accepted answer is it doesn't check for other characters. I know there will be a case where the currency symbol will be in the box because my user doesn't know better. So yeah I can check in javascript to remove it, but what if for some reason javascript isn't on? Then extra characters might get through. Or if someone tries to spam you passing unknown characters through... who knows! So I decided to use a regex. It's a bit slower, tiny fraction slower - for my case it was 1,000,000 iterations of the regex took just under 3 seconds, while around 1 second to do a string replace on a coma and period. But seeing as I don't know what characters might come through, then I am happy for this slightest of performance hits.
public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
string modelName = bindingContext.ModelName;
string attemptedValue =
bindingContext.ValueProvider.GetValue(modelName).AttemptedValue;
if (bindingContext.ModelMetadata.IsNullableValueType
&& string.IsNullOrWhiteSpace(attemptedValue))
{
return null;
}
if (string.IsNullOrWhiteSpace(attemptedValue))
{
return decimal.Zero;
}
decimal value = decimal.Zero;
Regex digitsOnly = new Regex(#"[^\d]", RegexOptions.Compiled);
var numbersOnly = digitsOnly.Replace(attemptedValue, "");
if (!string.IsNullOrWhiteSpace(numbersOnly))
{
var numbers = Convert.ToDecimal(numbersOnly);
value = (numbers / 100m);
return value;
}
else
{
if (bindingContext.ModelMetadata.IsNullableValueType)
{
return null;
}
}
return value;
}
}
Basically, remove all characters that are not digits, for a string that isn't empty. Convert to decimal. Divide by 100. Return result.
Works for me.

Resources