Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent default binders for complex types?

I have a model binder for a custom type Money. The binder works fine, but the built in binding/validation is kicking in and marking data as invalid.

My binder looks like this:

public class MoneyModelBinder : DefaultModelBinder 
{
    protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var money = (Money)bindingContext.Model;
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Amount").AttemptedValue;
        var currencyCode = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Iso3LetterCode").AttemptedValue;

        Money parsedValue;
        if (String.IsNullOrEmpty(value))
        {
            money.Amount = null;
            return;
        }

        var currency = Currency.FromIso3LetterCode(currencyCode);

        if(!Money.TryParse(value, currency, out parsedValue))
        {
            bindingContext.ModelState.AddModelError("Amount", string.Format("Unable to parse {0} as money", value));
        }
        else
        {
            money.Amount = parsedValue.Amount;
            money.Currency = parsedValue.Currency;
        }
    }
}

When a user types a value like "45,000" into a textbox my binder correctly picks up the value, parses it and sets it into the model.

The problem I have, is that the default validation then kicks in a states The value '45,000' is not valid for Amount, which as it's a decimal type makes sense, but I've already bound the data. How can I prevent the default binders binding data which I've already handled?

I'm not sure if this make any difference, but I'm using Html.EditorFor with and editor that looks like this:

@model Money

<div class="input-prepend">
  <span class="add-on">@Model.Currency.Symbol</span>
    @Html.TextBoxFor(m => m.Amount, new{
                            placeholder=string.Format("{0}", Model.Currency), 
                            @class="input-mini",
                            Value=String.Format("{0:n0}", Model.Amount)
                            })
    @Html.HiddenFor(x => x.Iso3LetterCode)
</div>
like image 933
ilivewithian Avatar asked Nov 14 '12 19:11

ilivewithian


3 Answers

You can just mark the Amount property as read only:

[ReadOnly(true)]
public decimal? Amount { get; set; }
like image 87
iHiD Avatar answered Nov 08 '22 12:11

iHiD


Provided that your ModelBinder code resides in the same project, you could change the property setter access modifier to internal. This way the DefaultModelBinder couldn't see the setter.

public class MyViewModel{
 public Money Stash { get; internal set; }
}

Another way could be simply the usage of a field

public class MyViewModel{
 public Money Stash;
}

The reasoning behind this is that DefaultModelBinder only binds read-write properties. Both suggestions above will prevent those conditions from being satisfied.

like image 31
Jani Hyytiäinen Avatar answered Nov 08 '22 12:11

Jani Hyytiäinen


There are several ways of doing this from my experience (allowing the default binder to bind, but clearing the errors on the property, for instance), but the way of doing this which makes your intent clear to anyone maintaining your code would be to override the undesired behavior, i.e.:

protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
    if (propertyDescriptor.Name != "Amount")
    {
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }
}

It might also make sense to actually do your custom binding in the else block here, but that's for you to decide.

like image 1
Kevin Stricker Avatar answered Nov 08 '22 12:11

Kevin Stricker