Showing posts with label ASP.NET MVC. Show all posts
Showing posts with label ASP.NET MVC. Show all posts

Wednesday, 16 December 2009

ASP.NET MVC Supporting Multiple Actions Through a Single Form

One of the things that always plagues me in ASP.NET MVC is supporting multiple actions through a single form.  Sometimes this is not a good idea, but often we need to do it to support multiple functions on the same data.  I’ve tried using javascript to override the form action but it takes a lot of complicated code and is much harder when using ajax calls. I’ve tried calling a single action and then splitting out the data to call the sub methods but unless you use redirections it makes your methods much harder to test and you lose your form if you redirect, plus the code is messy.

Finally I can across this post by MAARTEN BALLIAUW.  It specifies that you can use a custom attribute to determine which action is valid depending on other parameters.  It’s very smart and I highly suggest you read it, in fact I’m going to assume at this point that you have.

I took it a little bit further and have some hints for you.  You’ll likely get an error first time you try it saying that there is ambiguity between the actions, be sure that your form is calling an action that does not exist.  This code is going to decide if an action is valid, if the Index action is valid and the form post variable also says your post action is valid then it’s going to throw this exception.  An action that doesn’t exist can’t be valid, so only the one related to your form post variable will be valid.

Also, I don’t like to use the value of the submit button.  The value is also what the user sees and is likely to be changed by the business.  For this reason all I want to do is verify that the form variable was submitted.

I’ve changed the code in the attribute to read:

   1: public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
   2: {
   3:    //return controllerContext.HttpContext.Request[MatchFormKey] != null &&
   4:    //    controllerContext.HttpContext.Request[MatchFormKey] == MatchFormValue;
   5:    // Commented out because we don't want to match the value, just the key.
   6:  
   7:    if (MatchFormValue == null)
   8:    {
   9:        return controllerContext.HttpContext.Request[MatchFormKey] != null;
  10:    }
  11:    else
  12:    {
  13:        return controllerContext.HttpContext.Request[MatchFormKey] != null &&
  14:            controllerContext.HttpContext.Request[MatchFormKey] == MatchFormValue;
  15:    }
  16: }

It’s a fantastic solution so far, my thanks to Maarten.

Tuesday, 3 February 2009

ASP.NET MVC RC1 and Areas

I just found out something today that is very important if you have implemented areas in ASP.NET MVC as per this post by Phil Haack.  With RC1, strongly typed views that do not have code behind files (as per the RC1 setup) will throw an error:

UPDATE: Parser Error Message: Could not load type 'System.Web.Mvc.ViewPage<ModelObject>'

The problem is that your view folders within each of the areas does not have a Web.config file like your root view folder.  With RC1, the Web.config file in the root folder needs to have this:

<pages validateRequest="false"
   pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
   pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
   userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
    <controls>
        <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
    </controls>
</pages>

The simple solution is to make a Web.config file for each of the Views folders in your areas that is exactly the same as the one in your root view path.  Things will start working after that.

Monday, 1 December 2008

Unit Testing the UpdateModel method in ASP.NET MVC by Faking the Controller Context

One thing that’s not immediately obvious is that the UpdateModel method in ASP.NET MVC Beta requires a controller context to work.  In fact if you canll UpdateModel without controller context then you’re going to get an error like this:

threw exception:  System.ArgumentNullException: Value cannot be null.  Parameter name: controllerContext.

A quick search found that you need to fake the controller context to be able to unit test with UpdateModel, something Scott Gu doesn’t cover in his blog.  Never mind, Scott Hanselman to the rescue!  You will need to fake your controller context to get this to work correctly.  On Scott Hansleman’s blog he creates a MvcMockHelper class designed to mock certain aspects of the MVC environment, including the controller context.

This is the MvcMockHelpers class from Scott’s blog.

using System;
using System.Web;
using Rhino.Mocks;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Specialized;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace UnitTests
{
    public static class MvcMockHelpers
    {
        public static HttpContextBase FakeHttpContext(this MockRepository mocks)
        {
            HttpContextBase context = mocks.PartialMock<httpcontextbase>();
            HttpRequestBase request = mocks.PartialMock<httprequestbase>();
            HttpResponseBase response = mocks.PartialMock<httpresponsebase>();
            HttpSessionStateBase session = mocks.PartialMock<httpsessionstatebase>();
            HttpServerUtilityBase server = mocks.PartialMock<httpserverutilitybase>();
 
            SetupResult.For(context.Request).Return(request);
            SetupResult.For(context.Response).Return(response);
            SetupResult.For(context.Session).Return(session);
            SetupResult.For(context.Server).Return(server);
 
            mocks.Replay(context);
            return context;
        }
 
        public static HttpContextBase FakeHttpContext(this MockRepository mocks, string url)
        {
            HttpContextBase context = FakeHttpContext(mocks);
            context.Request.SetupRequestUrl(url);
            return context;
        }
 
        public static void SetFakeControllerContext(this MockRepository mocks, Controller controller)
        {
            var httpContext = mocks.FakeHttpContext();
            ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller);
            controller.ControllerContext = context;
        }
 
        static string GetUrlFileName(string url)
        {
            if (url.Contains("?"))
                return url.Substring(0, url.IndexOf("?"));
            else
                return url;
        }
 
        static NameValueCollection GetQueryStringParameters(string url)
        {
            if (url.Contains("?"))
            {
                NameValueCollection parameters = new NameValueCollection();
 
                string[] parts = url.Split("?".ToCharArray());
                string[] keys = parts[1].Split("&".ToCharArray());
 
                foreach (string key in keys)
                {
                    string[] part = key.Split("=".ToCharArray());
                    parameters.Add(part[0], part[1]);
                }
 
                return parameters;
            }
            else
            {
                return null;
            }
        }
 
        public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod)
        {
            SetupResult.For(request.HttpMethod).Return(httpMethod);
        }
 
        public static void SetupRequestUrl(this HttpRequestBase request, string url)
        {
            if (url == null)
                throw new ArgumentNullException("url");
 
            if (!url.StartsWith("~/"))
                throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\".");
 
            SetupResult.For(request.QueryString).Return(GetQueryStringParameters(url));
            SetupResult.For(request.AppRelativeCurrentExecutionFilePath).Return(GetUrlFileName(url));
            SetupResult.For(request.PathInfo).Return(string.Empty);
        }
       
    }
}

Now all I need to do is

_Mocks = new MockRepository();
_ItemRepository = _Mocks.StrictMock<IItemRepository>();
 
SetupTestData(_Mocks, _ItemRepository);
_Target = new StockItemMasterController(_ItemRepository);
 
MvcMockHelpers.SetFakeControllerContext(_Mocks, _Target);

Set the controller context to the faked context and my tests will all start magically working.

Updating the Model Object from the Form Post Variables

We were having trouble controlling the form post variables that are used in MVC to update our model.  How do you know which fields for your model have been updated and which fields are just defaulted to null on an action?  One option is to have each of the fields being updated as a parameter to the action, but this is messy and requires a lot of manual intervention to setup your model again.

The best solution we’ve found is to use the form collection.  This post by Scott Gu on the MVC Beta release overviews how it can be done.  I’m going to go into a little more detail specific to how that might be useful.

This method is fantastic for allowing you to specify which fields have changed in the post and updating the model object like so.

public ActionResult Add(FormCollection form)
{
    ItemMaster itemMaster = new ItemMaster();
 
    try
    {
        TryUpdateModel(itemMaster, form);
 
        List<ValidationErrorResult> results = _ItemRepository.Add(itemMaster.Item);
 
        if (results.Count > 0)
        {
            // TODO: setup the validation failures.
            return View(typeof(Views.en.StockItemMaster.Index), itemMaster);
        }
        else
        {
            return RedirectToAction("Index/" + itemMaster.Item.Name.ToString());
        }
    }
    catch (Exception e)
    {
        HandleIt.CatchTheException(e);
        return (View(typeof(Views.en.StockItemMaster.Index), itemMaster));
    }
}

On MVC View all you need to do is specify the input fields that are to be used to update the model and the item will be updated for you.  In my case the ItemMaster is my model object for a strongly typed view.  My form looks something like this:

<table class="TableFullWidth">
<tr>
    <td class="ColumnRightAlign">
        <label class="FieldLabel" for="Item.Name">
            Part No.</label>
    </td>
    <td>
        <input name="Item.Name" style="width: 75px" value="<%= ViewData.Eval("Item.Name") %>"
            FocusOnMe="true" &lt;%= ViewData.Eval("ItemKeyFieldSearch") %&gt; />
    </td>
    <td colspan="4">
        <input name="Item.Description" style="width: 275px" value="<%= ViewData.Eval("Item.Description") %>" />
    </td>
</tr>

Note: See we’re not using HTML Helpers for this view.  There is a good reason for that which I’m not going into in this post.

The input tags are wrapped up in a form with the action set to the Add method.  When I call TryUpdateModel the model will update and the ModelState will be populated with the exceptions.  The following test is a great example.

FormCollection form = new FormCollection();
form.Add("Item.ItemCodeID", "string");
ResetController();
ViewResult result = (ViewResult)_Target.Save(_TestItems[0].ItemCodeID, form);
Assert.AreEqual("StockItemMaster/Index", result.ViewName);
Assert.AreEqual(_TestItems[0].Name, ((ItemMaster)result.ViewData.Model).Item.Name);

This is a problem that because the ItemCodeID field is an integer field and I’ve set a string into the form post variable.  If I breakpoint my code and investigate the result you can see the error:

image

You can see the error message noting that the value “string” for your form is invalid.  If we were to use UpdateModel instead of TryUpdateModel, we would be catching an exception for each of these errors. 

It’s worth noting that UpdateModel is not a very good idea because if you’re dealing with the Entity Framework then your EntityKey field is going to cause an error every time unless you happen to post a form variable with the key in it. 

You can now use the ModelState in your view to display all your validation errors specific to the database or data model you’re using.

Thursday, 13 November 2008

Testable LINQ to Entities with Table Joins

I’m enjoying LINQ to Entities and the Entity Framework, but sometimes it feels like someone forgot to write up the documentation.  It’s nice to have the product, but it can be very hard to use without adequate training or documentation.  But I suppose it’s very new and I’ll just have to wait for more people to jump on board.

In the mean time, I came across a problem.  We are following the repository model design pattern in our data access objects and when using LINQ to Entities we define the table that the model is concerned with as a private member from the Entity Model.  This allows us to use TDD and override this private member with our own queryable datasets for testing.

So your data repository will look something like this:

public class SecurityUserRepository : ISecurityUserRepository
{
    private IQueryable<rfSysAction> _rfSysActionData;
    private MyEntities _Entities;
 
    public SecurityUserRepository()
    {
        _Entities = new MyEntities();
        _rfSysActionData = _Entities.rfSysAction;
    }
 
    public SecurityUserRepository(IQueryable<rfSysAction> sysAction)
    {
        _rfSysActionData = sysAction;
    }
}

Nice code, easy to test, but what if I want to join several tables together to get a set of data back?  For example, my user is part of several security groups, my security groups are linked to my MVC actions and my actions are defined in a table specifying their names. 

Well it’s actually not that hard.

Most often when trying to write this join you’ll get examples that make you reference the entity model to get all the tables you want to join.  This works fine, but it’s not good for me because I have only mocked up a copy of my root object.

Then I found this great example showing how it can be done.  So if I want to return an action table record for a specific action, controller and user my LINQ should look like this:

IQueryable<rfSysAction> query = 
    from sa in _rfSysActionData
        from sas in sa.rfSysActionSecurity
        from usg in sas.rfSecurityGroup.UserSecurityGroup
    where usg.UserID == userIdparam 
        && sa.Controller == controller
        && sa.Name == action
    select sa;

This query will find all security actions and then limit the selection to those for the user, controller and action specified.  This is what my model looks like for the tables in the above query:

image

The code is simple and makes sense when you know what to do, but now that I have it, how do I test it.  With rfSysAction as my base table, all I have to do is mock this up and add in the child table data like this:

private IQueryable<rfSysAction> GetTestUserSecurityGroupData()
{
    List<rfSysAction> data = new List<rfSysAction>
    {
        new rfSysAction()
        {
            ActionID = 1,
            Controller = "controllera",
            Name = "actiona",
            rfSysActionSecurity = new EntityCollection<rfSysActionSecurity>()
            {
                new rfSysActionSecurity()
                {
                    rfSecurityGroup = new rfSecurityGroup()
                    {
                        Name = "groupa",
                        UserSecurityGroup = new EntityCollection<UserSecurityGroup>()
                        {
                            new UserSecurityGroup()
                            {
                                UserID = 1
                            }
                        }
                    }
                }
            }
        }
    };
 
    return data.AsQueryable();
}

And then use the override constructor to set the data:

ISecurityRepository target = new SecurityRepository(GetTestUserSecurityData());

There you have it, testable LINQ to Entities and I haven’t had to overwrite several table object.