Archive

Posts Tagged ‘MVC’

Post a collection of ViewModel’s to a MVC Action with jQuery

December 7th, 2011 No comments

Maybe I searched for the wrong thing, but I couldn’t find what I was looking for Sad smile My Bing and Google fu failed me.

Basically I wanted to post a collection of ViewModels to an MCV action. Turns out it’s rather simple.

Lets say I have a bunch of Products, and Products are managed in a WarehouseLocation. A product doesn’t have a warehouse location, since it could exist in multiple locations.

If I’m currently working in Location A, I want to post a collection of Products to an action, as well as the WarehouseLocationId.

So given a simple ViewModel, and an Action:

public class ProductViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

and

public JsonResult Update(int warehouseLocationId, IEnumerable<ProductViewModel> products)
{
    //Do something with the products…

    return Json(new {Staus = "success"});
}

I think usually when someone sends data from jQuery it’s usually a single parameter,so the JSON would look something like:

var data = { id: 1,name: ‘test name’, price: 15.95 };

This would populate an action that looked like:

public JsonResult Update(ProductViewModel product)

But what I was faced with was passing in two parameters, one of which is a collection…

MVC seems to pair up the posted result with the parameter name, in the same way they it does for Route parameters. So to the JSON needs to look like:

var data = {
    warehouseLocationId: 12,
    products: [{
        Id: 1,
        Name: "Product 1",
        Price: 15.95
    }, {
        Id: 3,
        Name: "Product 2",
        Price: 12.50
    }]
};

As you can see the key’s on the first level match the parameter names, while the array on products, match the ViewModel.

Taking this exact data and posting it to the action:

$.ajax({
        type: 'POST',
        url: '@Url.Action("Update", "Home")',
        data: JSON.stringify(data),
        contentType: 'application/json',
        success: function(result) {
            //Do something with result…
        },
        dataType: 'json'
    });

 

I submit that, and with a breakpoint on my action I see:

image

^ The warehouseLocationId…

image

^ 2 products with the values:

image

And:

image

The exact same data we defined in our JavaScript.

The really cool thing about this, is you can have nested collections also. Using the same scenario, but extending Product to have a collection of Categories like so:

public class ProductViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public IEnumerable<Category> Categories { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}

I can update the JSON object to include Category information on 1 of the two products:

var data = {
    warehouseLocationId: 12,
    products: [{
        Id: 1,
        Name: "Product 1",
        Price: 15.95
    }, {
        Id: 3,
        Name: "Product 2",
        Price: 12.50,
        Categories: [{
            Id: 1,
            Name: "Category 1"
        }, {
            Id: 1,
            Name: "Category 2"
        }]
    }]
};

And submit that in the same way as before, capturing the results in the Action we get the first product with null for the categories, since we didn’t define it as an empty array:

image

While the second Product has 2 items, the first item is Category 1, and the second item is Category 2.

image

image

And that’s it, easy peasy, sending a collection of ViewModels from jQuery to an MVC Action.

I <3 MVC Smile

Categories: MVC, jQuery Tags: , ,

MVC +Areas + Routes… Order of Routes Matter!

July 27th, 2011 3 comments

The order in which routes are registered, really is pretty damn important, otherwise it can have really strange side-effects.

I have a site, which has 3 areas, and no default… ‘non-area’.

  • Admin
  • Members
  • Site
    So rather than having the structure:

image

Where the root/default site is in the main directory, with two areas. I wanted this structure:

image

So no root/default, and just a ‘Site’ area which would be the default.

(Note: The default route is removed from Global.asax… for now)

This works perfectly fine if all the URL’s are accessed like:

http://localhost:147/Site/Home/Index

http://localhost:147/Admin/Home/Index

http://localhost:147/Member/Home/Index

If we run this in a browser we get the following:

image

These routes are registered automatically when you add an Area, in a *AreaName*AreaRegistration file.

image

The routes generated look like the following:

namespace RoutingIssue.Areas.Members
{
    public class MembersAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "Members";
            }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "Members_default",
                "Members/{controller}/{action}/{id}",
                new { action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

But with different AreaName’s of course. You get the idea.

Now what we want to do is change the Site route, to work from the root directly like:

http://localhost:147/

This is where my assumptions began to go wrong… I updated the route like so:

public override void RegisterArea(AreaRegistrationContext context)
{
    context.MapRoute(
        "Site_default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

So I’ve removed ‘Site’ from the path, and added the default controller.

This is where things started to get messy. At first, it worked, in my sample project. Then I added AutoFac, MiniProfiler, some Content, and a few other things. That’s when it all went pear shaped.

image

It seems to have lost the information about the routes for Admin/Member.

Creating a new project with it working, and with my current project. I wrote some
Trace.Write(“AreaName”) code into each AreaRegistration. The working project outputted:

Members
Admin
Site

However the second project, where it was failing, outputted:

Site
Members
Admin

So what’s happening is in my project, it registers the default route first without the Area:

context.MapRoute(
    "Site_default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Took me hours to figure this out. That MVC cannot guarantee the order of which Area’s are registered. The second two Area’s didn’t stand a chance.

The solution was to move the Site route to the global.asax file, and specify an Area on it. My Site registration looks like:

public override void RegisterArea(AreaRegistrationContext context)
{
    //context.MapRoute(
    //    "Site_default",
    //    "{controller}/{action}/{id}",
    //    new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    //);
    Trace.WriteLine("Site");
}

While my global.asax file has:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional },
        new string[] { "RoutingIssue.Areas.Site.Controllers" }
    ).DataTokens.Add("Area", "Site");
}

This means the default route is registered after my Areas, and everything works perfectly again Smile

Categories: MVC Tags: , ,