Saturday, July 26, 2008

Controller is NOT the New Code-Behind

One of the things that I often say when touting the benefits of ASP.NET MVC is clean separation of concerns.  I talk about how the code-behind of WebForms is just begging you to put code there.  Samples online frequently open direct database connections and execute ad-hoc queries against the database from a button-click event.

Of course the same is possible with ASP.NET MVC.  Just taking a peak at the new Preview 4 installer, you’ll see a brand-spanking new AccountController, pre-built for managing logins/membership on your site.

Open AccountController.cs and you’ll find this method:

    [Authorize]
    public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword)
    {

      ViewData["Title"] = "Change Password";
      ViewData["PasswordLength"] = Provider.MinRequiredPasswordLength;

      // Non-POST requests should just display the ChangePassword form 
      if (Request.HttpMethod != "POST")
      {
        return View();
      }

      // Basic parameter validation
      List<string> errors = new List<string>();

      if (String.IsNullOrEmpty(currentPassword))
      {
        errors.Add("You must specify a current password.");
      }
      if (newPassword == null || newPassword.Length < Provider.MinRequiredPasswordLength)
      {
        errors.Add(String.Format(CultureInfo.InvariantCulture,
                 "You must specify a new password of {0} or more characters.",
                 Provider.MinRequiredPasswordLength));
      }
      if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
      {
        errors.Add("The new password and confirmation password do not match.");
      }

      if (errors.Count == 0)
      {

        // Attempt to change password
        MembershipUser currentUser = Provider.GetUser(User.Identity.Name, true /* userIsOnline */);
        bool changeSuccessful = false;
        try
        {
          changeSuccessful = currentUser.ChangePassword(currentPassword, newPassword);
        }
        catch
        {
          // An exception is thrown if the new password does not meet the provider's requirements
        }

        if (changeSuccessful)
        {
          return RedirectToAction("ChangePasswordSuccess");
        }
        else
        {
          errors.Add("The current password is incorrect or the new password is invalid.");
        }
      }

      // If we got this far, something failed, redisplay form
      ViewData["errors"] = errors;
      return View();
    }

That’s nearly 70 lines in a single method.  Even with the comments it’s difficult to see what it’s doing at a glance.

This serves as a lesson to those of us that sell TDD as a design practice.  Sure this method is testable, but it seems as though the author did not take a few minutes to refactor code.  Normally I wouldn’t pick on someone for writing this, but when this is installed as a project template, it serves as an example for others to write code just like this.

Let’s see how we might clean this code up to make it more readable.

Here’s a good first attempt.  This took all of about 5 minutes to accomplish.

    [Authorize]
    public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword)
    {
        ViewData["Title"] = "Change Password";
        ViewData["PasswordLength"] = Provider.MinRequiredPasswordLength;        // Non-POST requests should just display the ChangePassword form 
        if (Request.HttpMethod != "POST")
        {
            return View();
        }
            
        var errors = new List<string>();
        ValidatePassword(currentPassword, newPassword, confirmPassword, errors);
        if (errors.Count == 0)
        {                
            const bool isOnline = true;
            var currentUser = Provider.GetUser(User.Identity.Name, isOnline);
                
            if( PerformPasswordChange(currentUser, currentPassword, newPassword))
            {
                return RedirectToAction("ChangePasswordSuccess");
            }
                
            errors.Add("The current password is incorrect or the new password is invalid.");               
        }

        // If we got this far, something failed, redisplay form
        ViewData["errors"] = errors;
        return View();
    }

    private bool PerformPasswordChange(MembershipUser user, string currentPassword, string newPassword)
    {
        try
        {
            user.ChangePassword(currentPassword, newPassword);
            return true;
        }
        catch
        {
            return false;
        }
    }

    private void ValidatePassword(string currentPassword, string newPassword, string confirmPassword, ICollection<string> errors)
    {
        if (String.IsNullOrEmpty(currentPassword))
        {
            errors.Add("You must specify a current password.");
        }

        if (newPassword == null || newPassword.Length < Provider.MinRequiredPasswordLength)
        {
            errors.Add(String.Format(CultureInfo.InvariantCulture,
                  "You must specify a new password of {0} or more characters.",
                  Provider.MinRequiredPasswordLength));
        }

        if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
        {
            errors.Add("The new password and confirmation password do not match.");
        }
    }

This code (while still completely in the controller class) is broken apart into distinct methods.  This reduces the line count on the action method to 26, which is much easier to swallow.  I’m sure you see even more opportunities for improvement here.  Some of this code probably belongs in another, completely separate class (one that is even more easily tested that this).

My point is, always remember to include time for refactoring.  Especially when the result of your work will stand for thousands to learn from.

Saturday, July 26, 2008 5:40:16 PM (Central Standard Time, UTC-06:00)
What was that big discussion you were having on Twitter last night about teaching people new patterns? I think it was DI/IOC specifically.

It just goes to show that even with a solid pattern like MVC, a lack of proper OO design and even a lack of TDD-style design can result in bad code.

There will still be bad developers, even with good frameworks and patterns.

So I agree with whichever side was saying that teaching solid OO design concepts should come before just learning a new pattern.

Saturday, July 26, 2008 9:17:14 PM (Central Standard Time, UTC-06:00)
Hi Ben,
We actually had some late-breaking improvements we wanted to make to the AccountController but didn't have a chance to get them into the release. Expect to see some improvements to the AccountController as well as the related unit tests. In MVC Preview 4 we wanted to get the functionality in but didn't have time to polish it. Thanks for the feedback since that encourages us to make sure we get the fixes in for the next release :)

Thanks,
Eilon
Saturday, July 26, 2008 11:06:29 PM (Central Standard Time, UTC-06:00)
I don't get it... That's really not that much of a revolutionary change. I personally would go with the something similar to what you have, but I do think you're being a bit picky. I think a worthwhile improvement would be to stick that into a service class method or into a "task" similar to the 'CreateAdminAccountTask' class in the (your?) Code Camp Server example.

While we're on the subject... I'm beginning to get a distaste for controllers. It seems like too much code in one place. I think I'd prefer the one controller per view that traditional pages (with code-behind) offer. It seems more logical and "connected". If you start to get controllers that do a lot more than a simple call to render a view, you start having a lot of code in a single controller class. Maybe it's just me, but this translates a lot of stepping on each others toes when you check a controller with a lot of actions back into source control. Maybe you have to be more granular with your controllers?

Apologies in advance for being stupid. :-)
Sunday, July 27, 2008 2:21:33 AM (Central Standard Time, UTC-06:00)
@Eric
"I'm beginning to get a distaste for controllers. It seems like too much code in one place." - just don't do that :) Your controllers from MVC, i.e. UI controllers, should do just that - orchestrate input\output. For "controllers that do a lot more" use "Use-case" controllers in your app layer code. So, there is a use-case in the system, which your app controller serves. UI controller calls into it.
Sunday, July 27, 2008 5:30:31 AM (Central Standard Time, UTC-06:00)
A couple of questions:
1. Is PerformPasswordChange() a wrapper around a bad model function?
2. No checking for bad login?
3. What's a "Use-case controller in the app layer code"?

Thanks of the refactorings, I find them very useful.
Sunday, July 27, 2008 9:34:49 AM (Central Standard Time, UTC-06:00)
@Eric:

You're right. I didn't change much. But I do think that I increased readability with my small change. I also believe that I can do better still. Bringing most of this out into a service method or a ChangePasswordTask class might also improve the code. The trick is to still be able to aggregate the errors.

@Dmitri:

PerformPasswordChange is kinda fugly, I agree. What do you mean a wrapper around a bad model function? As in the membership API? Personally I don't care for it, so I would probably wrap the whole thing.

@Eilon:

I have no doubt ... thanks for the replay DAWG!
Sunday, July 27, 2008 7:25:57 PM (Central Standard Time, UTC-06:00)
@Ben,

Nice post! Refacotrings like this definately need to be shown to the community so people can see how you can properly arrange controller actions.

@Eilon,

Sorry but, I feel like saying "we didn't have enough time" is a bit of a cop out. As Ben has shown above, those changes only took five minutes. I really feel that your team is in a privlidged position. How your staff communicates usage of this tool people will take literally. Without too much careful instruction controllers could very well be the next code-behinds which is what we don't want. I don't mean to be inflammatory here, I just feel very strongly about this.
Monday, July 28, 2008 10:24:21 AM (Central Standard Time, UTC-06:00)
@Eric

The one controller per view seems a bit unrealistic, especially when compared to webforms. Too many mutiple views per codebehind and were leveraging codebehinds to add controls dynamically ect. With webforms it was a grab back of what was going on "back there".

@Sean

I'm sure Elion was wanting to add more than just what Ben did. Cut the team some slack. In true SCRUM/agile fashion they're shipping on a reliable, and short, schedule. I'm okay for the most part with what they're doing. I'd rather have more often code drops than to wait an extended period of time for the new framework.
Comments are closed.