Tuesday, April 19, 2011

Please create a model for your inputs. Model guidelines for ASP.NET MVC

Please create a model for your inputs; even if a FormCollection or Request.Form seems expedient at some pressured moment(e.g. deadline), track down what causes the ASP.NET's MVC engine not populating your models. By using models, our program is more "discoverable" so-to-speak, so any new or existing programmers who will review or continue our code can easily infer the intent of code, they would be able to know what other informations goes with the model, FormCollection and Request.Form doesn't carry any business logic semantics, those who are new to our programs could not easily infer if FormCollection carries a Bonus field, they won't be able to know if we overlooked some fields, they won't be able to know if the program needed to include other fields when performing computations from data before saving the data to database.



What I said is not a draconian rule; really, I'm an old man trapped in a young body. If we still cannot figure out why MVC cannot populate our models well, we will just use FormCollection for the meantime(though not pansamantagal, there's something exhilirating tracking the causes of errors), but eventually we shall have to discover what causes ASP.NET MVC engine not populating our models well


"Young men know the rules, but old men know the exceptions." -- Oliver Wendell Holmes



One of the causes why ASP.NET MVC cannot populate the object's property well is when the field source is in invalid format. Example, passing Feb 31, 1976 to a birthday field. Another example is passing 1,234 to a decimal type, whoa..! wait! wait! 1,234 is a valid decimal, and a quick trip to SnippetCompiler showed me that Convert.ToDecimal("1,234") doesn't causes any runtime error, and it returns 1234. Unfortunately for us, web is a different beast, it is public-facing most of the time, so there's the multi-culture(i.e. different nations uses different money format) issues. For instance, a German string input(every inputs on web are strings) of 1,234 means 1 dollar(or euro) and 234 cents; 1.234 is one thousand two hundred thirty four bucks for them. So ASP.NET MVC team didn't make an assumption that a 1,234 string has a universal meaning for a decimal type.


There's a two solution on that issue, one is to clean the data prior to submitting:

$(function () {
    $('#theFormIdHere').submit(function() {
        var uxSalary = $('input[name=Salary]');
        uxSalary.val(uxSalary.val().replace(',', ''));
    });
});

But that causes an ugly artifact on UI, the decimal type's formatting on textbox will be altered. So this is a no-go proposition for some developers.


Another solution is to use a ModelBinder for decimal. In this case, we don't reinvent the wheel, let's just use Phil Haack's code. Get it here: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx


To see things in action and make 1,234 worked, download the code here: http://code.google.com/p/test-aspnetmvc-decimal-binding-problem/downloads/list

And do either of the following:

Uncomment line 22 to 27 of Index.cshtml
/*$(function () {
        $('#theDataHolder').submit(function() {
            var uxSalary = $('input[name=Salary]');
            uxSalary.val(uxSalary.val().replace(',', ''));
        });
    });*/

Or uncomment line 36 of Global.asax
// ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());



Regarding the duplication of model, one in WCF Data Services project, another in our web project. We are just being pragmatic, we are not able find a way to make WCF Data Services' models carry other semantics(e.g. validation attributes). I tried Scott Gu's buddy-class approach, but for the life of me I don't know why WCF Data Services can't carry data validation attributes I added to an entity in a WCF Data Services. The duplicity, as evident on FbPerson module, I'm not using WCF Data Services' FbPerson model on FbPerson's form; instead, I created a separate FbPerson model on our web project's Models folder and uses that model on FbPerson's form. Using that approach, in its barest essence, WCF Data Services merely acts as a data access layer, it can't (or we just can't figure it out yet) carry semantics loaded to models like Required, Range, etc.



Perhaps we have a better chance of loading semantics to models if we do a code-first(instead of using designers, just do POCOs) models on WCF Data Service, don't want? :-)


Another approach is to use WCF Service and also do a code-first of models, i.e. POCO; and let the ORM(say Entity Framework or NHibernate) persist those models to database. We can load semantics(lol I'm not contented with the word attribute/validation) to models using this approach; also by using this approach we can tap the full potential of an ORM, we can get server-generated id(e.g. newsequentialid) for example, and better handling of concurrency(of which I know NHibernate is capable of including rowversion field on UPDATE statement's WHERE clause, on Entity Framework I have yet to google it), I noticed we didn't introduce a rowversion field (for concurrency stuff) on all our tables :-)


Speaking of NHibernate stuff, did you know we can just merely do one statement for both insert and update with NH? We just push the model to it and it will do its job well. That method is Merge. And when NH's Merge is doing an UPDATE, it includes the rowversion(for handling concurrent update) field to WHERE clause as well, neat :-)

Example WCF Service using NHibernate:
public KeyAndRowversion FbPerson_Save(FbPerson fromInput)
{
    using(var s = OurSystem.SessionFactory.OpenSession())
    {
        // NHibernate doesn't alter the model you passed to it; instead, it returns a new object which contains the current state of your model
        FbPerson currentValues = (FbPerson) s.Merge(fromInput);
        
        return new KeyAndRowversion { HasError = false, GeneratedGuid = currentValues.PersonId, CurrentRowversion = currentValues.TheRowVersion };
    }
}

Contrast that with Entity Framework approach.

Example WCF Service using EntityFramework:
public KeyAndRowversion FbPerson_Save(FbPerson fromInput)
{
    using(var s = new OurSystem.ErpDbContext())
    {
        FbPerson fromExisting = s.FbPersons.Find(fromInput.PersonId);

        if (fromExisting == null)
        {
            s.Persons.Add(fromInput);
        }
        else
        {
            fromExisting.Firstname = fromInput.Firstname;
            fromExisting.Lastname = fromInput.Lastname;
            fromExisting.Age = fromInput.Age;
            fromExisting.CountryId = fromInput.CountryId;
        }

        s.SaveChanges();

        // Entity Framework alters the model you passed to it, example: primary key; I haven't checked yet if EF also loads the current rowversion  on altered model
        return new KeyAndRowversion { HasError = false, GeneratedGuid = fromInput.PersonId };        
    }
}

No comments:

Post a Comment