1

I have a view model with nested Size object:

public class RedirectViewModel
{
    public Size Size { get; set; }
}

Size class:

public class Size
{
    public int Height { get; set; }

    public int Width { get; set; }
}

I would like to have properties of the Size object filled by the route values I'm passing using redirection.

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return RedirectToAction(nameof(Redirect), new { Size = new { Width = 20, Height = 40 } } );
    }

    public IActionResult Redirect([FromRoute] RedirectViewModel viewModel)
    {
        return View(viewModel);
    }
}

What I am getting out of this is a redirection URL with query parameters:

?Size=%7B%20Width%20%3D%2020,%20Height%20%3D%2040%20%7D

which URL decoded comes to:

?Size={ Width = 20, Height = 40 }

Yes, the spaces are there too, I did not add them for clarity.

Sadly, this does not work. No properties are filled. But if I manually type in parameters written in such way:

?Size.Width=20&Size.Height=40

then the model binder will successfully recognize them and as a result will set the corresponding properties, as opposed to mentioned earlier URL.

I don't want to manually tweak over query parameters to get it working.

Question

How to construct the route values for properties of a nested object, so model binder recognizes them?

EDIT:

To ease the understatement of my problem I did not use the exact objects I was using. The Size struct I used in original code sample was somehow not binding correctly, so I changed it too my own class which is closer to the original scenario. Apologizes for that, I should have checked it before posting a question. Original question still states unchanged.

EDIT #2:

I found similar question for ASP.NET MVC 5. @ant-p answer suggest using RouteValueDictionary and then passing it to route values. It is not perfect, because it involves manually writing property names, which doesn't have to be bound using the exact property name e.g. when annotated with FromQueryAttribute.

Prolog
  • 2,698
  • 1
  • 18
  • 31
  • why don't you just populate the viewmodel and pass it instead? – markorial Jan 23 '19 at 12:15
  • @markorial It is not that simple. Passing a single object into route values will result in listing all available properties from that object. If I pass `Size` object from the post I will end up with `?IsEmpty=False&Width=20&Height=40` This does not work, because these are properties of `Size` not `RedirectViewModel`. Not to mention that such solution will ignore possible routing attributes e.g. `FromQuery`. – Prolog Jan 23 '19 at 12:41
  • @Prolog the viewmodel in question is RedirectViewModel. Why don't you pass a RedirectViewModel ? As for using `System.Drawing.Size` in a web application, don't. It's not a DTO, it's a class specifically built for drawing – Panagiotis Kanavos Jan 23 '19 at 13:33
  • @panagiotis-kanavos Because it does not work. I end up with `?Size=WebAppTest.Models.Size`. Most probably the `ToString()` method was called on `Size` object which is quite expected behaviour. – Prolog Jan 23 '19 at 13:43
  • @Prolog that's not how routing works. It doesn't call `ToString()` on the route objects. In any case you're trying to do several unusual things at the same time. Query parameters have no notion of objects or lists. Anything other than `name=value;` is an application convention. They are *not* meant to transfer complex objects. That's what the body of a POST request is about. – Panagiotis Kanavos Jan 23 '19 at 14:02
  • @Prolog when you added `FromQuery` you told MVC that the `viewModel` parameter should be built from query parameters whose names match the object's properties. That's why `Size.Width=20&Size.Height=40` worked. – Panagiotis Kanavos Jan 23 '19 at 14:03
  • @Prolog this is getting interesting. Binding is performed by [binders](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.imodelbinder?view=aspnetcore-2.2). FromRoute specifies where the values come from, not which binder to use. Based on the name, I suspect [ComplexTypeModelBinder](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.complextypemodelbinder?view=aspnetcore-2.2) is used to bind complex types. – Panagiotis Kanavos Jan 23 '19 at 14:22
  • That binder's [unit tests](https://github.com/aspnet/AspNetCore/blob/f113a20dfd9d0c7c991e425fd8000ed6b64d19a5/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs#L1481) show it can handle a nested type when the property is marked with a custom attribute that provides extra metadata to the provider through its `BindingSource` property – Panagiotis Kanavos Jan 23 '19 at 14:24
  • @panagiotis-kanavos Thank you for all information, it really helped me to understand what's going on here. But... is there any chance to use now all that knowledge to write something that translates the complex object I'm trying to pass to a format recognizable by the ComplexTypeModelBinder? – Prolog Jan 23 '19 at 14:37

1 Answers1

0

Simply you can do as follows:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return Redirect(new Size() { Width = 20, Height = 40 } );
    }

    public IActionResult Redirect(Size size)
    {
        return View(size);
    }
}
TanvirArjel
  • 30,049
  • 14
  • 78
  • 114
  • I think that's not true. In my question I stated that `?Size.Width=20&Size.Height=40` is working correctly, that means, model binder recognizing the names for properties of a nested object and filling them with values. See my updated question, maybe it changes something for you. – Prolog Jan 23 '19 at 13:05
  • Already tried. It can't be done due to compile time error: "_Invalid anonymous type member declarator. Anonymous type members must be declared with a member assignment, simple name or member access._". Names of properties in anonymous types have to be simple. – Prolog Jan 23 '19 at 13:15
  • @Prolog I have found a solution. I have tested this and it works perfectly. Please see my updated answer. – TanvirArjel Jan 23 '19 at 13:29
  • @TanvirArjel and the solution is? People that can't see the edit history won't be able to tell what you changed. In this case, you added `FromRoute` to the parameter. The OP already used `FromRoute` though – Panagiotis Kanavos Jan 23 '19 at 13:37
  • You can remove this. – TanvirArjel Jan 23 '19 at 13:38