ASP.NET Core- Is there a way to render group of elements each using custom tag-helpers? - asp.net-core

I noticed in my project that all my form fields follow the same pattern. A typical example is:
<div class="col-x-x">
<label asp-for="Property"></label>
<span message="description">
<input asp-for="Property" />
<span asp-validation-for="Property"></span>
</div>
I would love to have some way of grouping this code so that i simply pass it the property on the model and it outputs the correct HTML. e.g.:
<form-field for="Property" ...>
or
#Html.StringFormField(...)
The issue I am having is that whatever method I try, the html outputted is the original html above, and not the html that is generated from the tag helpers. I have tried both methods and neither have been successful. Additionally I have tried to create a razor function, but all my attempts fail to compile, and I can't make a partial view work as I haven't been able to find a way to get the property information after passing a string to a view.
My latest attempt was using a tag helper, however this had the same issue mentioned previously. The latest version of the code is as follows:
[HtmlTargetElement("form-field", Attributes = "for")]
public class FormFieldTagHelper : TagHelper
{
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
var contentBuilder = new HtmlContentBuilder();
contentBuilder.AppendHtmlLine($"<label asp-for=\"{For}\"></label>");
contentBuilder.AppendHtmlLine($"<span message=\"description.\"></span>");
contentBuilder.AppendHtmlLine($"<input asp-for=\"{For}\"/>");
contentBuilder.AppendHtmlLine($"<span asp-validation-for=\"{For}\"/></span>");
output.Content.SetHtmlContent(contentBuilder);
}
}
There is an issue addressing this (with no solution) which suggested the order of the imports was a potential issue, so my imports are as follows:
#addTagHelper Project.Web.Features.Shared.*, Project.Web
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Any solution would be welcome, either for a tag helper or another method.

You could use IHtmlGenerator to generate these elements, refer to my below demo code:
[HtmlTargetElement("form-field", Attributes = "for")]
public class FormFieldTagHelper : TagHelper
{
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }
private readonly IHtmlGenerator _generator;
[ViewContext]
public ViewContext ViewContext { get; set; }
public FormFieldTagHelper(IHtmlGenerator generator)
{
_generator = generator;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
using (var writer = new StringWriter())
{
writer.Write(#"<div class=""form-group"">");
var label = _generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Name, null,
new { #class = "control-label" });
label.WriteTo(writer, NullHtmlEncoder.Default);
writer.Write(#"<span message=""description.""></span>");
var textArea = _generator.GenerateTextBox(ViewContext,
For.ModelExplorer,
For.Name,
For.Model,
null,
new { #class = "form-control" });
textArea.WriteTo(writer, NullHtmlEncoder.Default);
var validationMsg = _generator.GenerateValidationMessage(
ViewContext,
For.ModelExplorer,
For.Name,
null,
ViewContext.ValidationMessageElement,
new { #class = "text-danger" });
validationMsg.WriteTo(writer, NullHtmlEncoder.Default);
writer.Write(#"</div>");
output.Content.SetHtmlContent(writer.ToString());
}
}
}
View:
<form-field for="ManagerName"></form-field>
Result:

It seems the easiest way to do this without duplicating custom tag helper code with the html generator is by simply creating new instances of the custom tag helpers from within a new tag helper.
e.g.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
//create label tag
LabelForTagHelper labelTagHelper = new LabelForTagHelper(ValidatorFactory)
{
For = this.For,
IgnoreRequired = this.IgnoreRequired
};
TagHelperOutput labelOutput = new TagHelperOutput(
tagName: tagName,
attributes: attributes ?? new TagHelperAttributeList(),
getChildContentAsync: (s, t) =>
{
return Task.Factory.StartNew<TagHelperContent>(() => new DefaultTagHelperContent());
}
);
var labelElement = await labelTagHelper.ProcessAsync(context, labelOutput);
output.Content.AppendHtml(labelElement );
//repeat for other tags
}

Related

Get full HTML field name for client side validation in ASP.NET Core

I'm implementing a custom validation attribute. This attribute does not only look at the value of the property it is applied to, but also at the value of another property. The other property is specified by its name.
I need to find a way to get the full id that the input for the other property will have in the final HTML output.
This is a simplified version of my validation attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class MyCustomValidationAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
public MyCustomValidationAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
var otherProperty = context.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(context.ObjectInstance, null));
// Validation logic...
}
public void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-mycustomvalidation", errorMessage);
// THIS ROW NEEDS TO BE FIXED
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
}
private void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (!attributes.ContainsKey(key))
{
attributes.Add(key, value);
}
}
}
This demonstrates how it is used in a model class:
public class Report
{
[MyCustomValidation("Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
This is the JavaScript to make sure that the validation is also done on the client side:
$.validator.addMethod('mycustomvalidation',
function (value, element, parameters) {
var otherPropertyValue = $('#' + parameters.otherpropertyname).val();
// Validation logic...
});
$.validator.unobtrusive.adapters.add('mycustomvalidation', ['otherpropertyname'],
function (options) {
options.rules.mycustomvalidation = options.params;
options.messages['mycustomvalidation'] = options.message;
});
My viewmodel for the page/view with the form looks like this:
public MyViewModel
{
public Report MyReport { get; set; }
}
Note that I'm not using Report as my viewmodel, but rather as the type of a property in the viewmodel. This is important since this is the root of my problem...
The code in the view to show the input for Value1 is nothing strange (I'm using Razor Pages):
<div>
<label asp-for="MyReport.Value1"></label>
<input asp-for="MyReport.Value1" />
<span asp-validation-for="MyReport.Value1"></span>
</div>
And the output becomes:
<label for="MyReport_Value1">Value1</label>
<input
type="text"
id="MyReport_Value1"
name="MyReport.Value1"
data-val="true"
data-val-mycustomvalidation="Error..."
data-val-mycustomvalidation-otherpropertyname="Value2"
value=""
>
<span
data-valmsg-for="MyReport.Value1"
data-valmsg-replace="true"
class="text-danger field-validation-valid"
></span>
So the problem is that in the HTML output I need data-val-mycustomvalidation-otherpropertyname to be "MyReport_Value2" instead of just "Value2". Otherwise the validation code won't be able to find the second HTML input (with id MyReport_Value2) and perform the validation.
I figure this must be done in the method AddValidation() in the attribute class, but how do I get the full name that the HTML input will recieve?
I'm guessing there is some way to get this by using the context parameter. I've seen examples of something like "*.TemplateInfo.GetFullHtmlFieldId(PropertyName)" but I can't get it to work.
Any help is appreciated!
You pass Value2 to MyCustomValidationAttribute and set _otherPropertyName with Value2,and use
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
so that html will be
data-val-mycustomvalidation-otherpropertyname="Value2"
You only need to pass Report_Value2 to MyCustomValidationAttribute rather than Value2.
public class Report
{
[MyCustomValidation("Report_Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
So that you will get
data-val-mycustomvalidation-otherpropertyname="Report_Value2"
ValidationContext is binded to instance that belong to validating property i.e Model. Hence locating reference of ViewModel looks difficult.
I can provide three different solution you can use which one suits your requirement.
Solution 1:
Using ValidationContext you can able to get Name of the class where Property belong to. This solution will work only if ViewModel Property Name must be same as Model Class Name.
e.g. if Model Class is Student then property name must be Student. If property name is Student1 it wont work.
Solution 2 & 3 will work even if Class name and property name are different.
Model
public class Student
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required]
[Country("Name")]
public string Country { get; set; }
}
ViewModel
public class StudentViewModel
{
public Student Student {get;set;} //Solution 1 wil not work for Student1
}
ValidationAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class CountryAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
private string _clientPropertyName;
public CountryAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(validationContext.ObjectInstance, null));
_clientPropertyName = otherProperty.DeclaringType.Name +"_"+ otherProperty.Name;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", _clientPropertyName);
}
}
Solution 2:
Using ClientModelValidationContext you can able to get ViewModel reference that is passed from the controller to view. By using reflection we can get the name of the property i.e Model.
To work with solution you need to pass empty ViewModel reference from controller.
Controller
public IActionResult New()
{
StudentViewModel studentViewModel = new StudentViewModel();
return View(studentViewModel);
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var viewContext = context.ActionContext as ViewContext;
if (viewContext?.ViewData.Model is StudentViewModel)
{
var model = (StudentViewModel)viewContext?.ViewData.Model;
var instanceName = model.GetType().GetProperties()[0].Name;
otherClientPropName = instanceName + "_" + otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
Solution 3:
Using context.Attributes["id"] you can able to get current property id value as string . By using string manipulation you can get prefix then you can merge with other property name.
This solution doesn't require empty ViewModel reference from controller.
Controller
public IActionResult New()
{
return View();
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var id = context.Attributes["id"];
var idPrefix = id.Split("_");
if (idPrefix.Length > 1)
{
otherClientPropName = idPrefix[0] + "_" + otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
HTML Output
<input class="form-control" type="text" data-val="true" data-val-required="Please enter name" id="Student_Name" name="Student.Name" value="">
<input class="form-control input-validation-error" type="text" data-val="true" data-val-mycustomvalidation-otherpropertyname="Student_Name" data-val-required="The Country field is required." id="Student_Country" name="Student.Country" value="">
This is a method that also works when there are fields rendered that are deeper children of the model.
//Build the client id of the property name.
var dependentClientId = dependentPropertyName;
var clientId = context.Attributes["id"];
var clientIdArr = clientId.Split("_");
if (clientIdArr.Length > 1)
{
//Replace the last value of the array with the dependent property name.
clientIdArr[clientIdArr.Length - 1] = dependentPropertyName;
dependentClientId = string.Join("_", clientIdArr);
}
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", dependentClientId );

how can I refer to a component created via DynamicComponent in Blazor?

I'm rendering components using DinamicComponent and I need to call a function found in the child component.
I can't find the equivalent of using #ref for the DinamicComponents so that I can reference to call the function.
This is the parent component
<div class="tab-content">
#foreach (VerticalTabComponent.TabModel oneTabItem in VerticalTabsList)
{
<div class="tab-pane fade show #(oneTabItem.TabIndex == SelectedTabIndex ? "active" : "")" #key=#($"VTabDivDynamic_{TabPrefix}_{oneTabItem.TabIndex.ToString()}")>
<DynamicComponent
Type=#System.Type.GetType(oneTabItem.TabComponent)
Parameters=#oneTabItem.TabParameters>
</DynamicComponent>
</div>
}
</div>
This is the code in Blazor Component Tab
public partial class TabComponent
{
[Parameter]
public EventCallback<string> InsertUpdateCallback { get; set; }
protected override async Task OnInitializedAsync()
{
await CallAnyfunctionAsync();
}
private async Task<bool> LoadDataGrid()
{
//this is the function I need to call from parent
}
}
How can I call the Load Grid function from the parent component?
There is an easy solution. Not sure if that is new but the ref-attribut does exist for the DynamicComponent! You can use it like this:
<DynamicComponent Type="typeof(MyComponent)" Parameters="#MyParameters" #ref="dc" />
and in Code-Behind:
private DynamicComponent? dc;
private MyComponent? MyComponentRef
{
get
{
return (MyComponent?)dc?.Instance;
}
}
Normally in Blazor we use #Ref to get a reference to a component, but as you've seen this won't work with a DynamicComponent.
A workaround for this would be to add a [Parameter] to the component called something like Register which is an action with the generic type set as the component type. You can then add code to handle OnParametersSet to call this method in the component.
You can then add a Register parameter in your TabParameters which gets updated with a reference.
Example code below would be added to the SurveyPrompt component:
/// <summary>
/// will be called when params set
/// </summary>
[Parameter] public Action<SurveyPrompt> Register { get; set; }
protected override void OnParametersSet()
{
if (Register != null)
{
// register this component
Register(this);
}
}
You add a Register parameter with an Action<type> value. Here's an example:
SurveyPrompt sp1 = null;
void Register1(SurveyPrompt survey)
{
sp1 = survey;
Console.WriteLine("SP1 has title " + sp1.Title);
}
protected override void OnInitialized()
{
Action<SurveyPrompt> p1 = Register1;
params1 = new Dictionary<string, object>()
{
{ "Title", "Survey Title Here" },
{ "Register", p1 }
};
}
IDictionary<string, object> params1;

How to generate a Razor Page url within a custom TagHelper

I have a custom tag helper which should render something like this:
<ol>
<li>Some text
</ol>
If I were to do this within a Razor Page I would simply do something like this: <a asp-page="MyRazorPage">Some text</a>
Is there a way to do something similar inside of the TagHelper?
I found the answer.
Inject IUrlHelperFactory into the constructor as well as use the following property:
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
Then you can create an IUrlHelper this way:
var urlHelper = _urlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Page("/Clients/Edit", new { Id = myClientId });
output.Content.AppendHtmlLine($"<a href='{url}'>Edit</a>");
TagHelper provides HtmlTargetElement to add attributes to specified tags. Take adding asp-cuspage to the tag <a> as an example. The method Init is used to receive the parameters in the instruction asp-cuspage="". This method Process provides output attributes.
Create class CusAnchorTagHelper:
[HtmlTargetElement("a")]
public class CusAnchorTagHelper : TagHelper
{
private const string CuspageAttributeName = "asp-cuspage";
[HtmlAttributeName(CuspageAttributeName)]
public string Cuspage { get; set; }
public string Value { get; set; }
public override void Init(TagHelperContext context)
{
if (context.AllAttributes[0].Value != null)
{
Value = context.AllAttributes[0].Value.ToString();
}
base.Init(context);
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var reg = new Regex("(?<!^)(?=[A-Z])");
string attr="";
foreach(var a in reg.Split(Value))
{
attr += a + "/";
}
output.Attributes.SetAttribute("href", attr);
}
}
Then, inject custom taghelper assembly into the page. And it will be drawn in the view.
This is the rendered result.

ASP MVC ActionLinks with HTML content

There are nice extension methods to generate ActionLinks/RouteLinks in ASP MVC. However it only lets you write plain text inside the generated tag. What if you want to generate anchor with image inside?
I wanted to create links using the icons from bootstrap:
// expected result
<i class="glyphicon glyphicon-arrow-left"></i> Previous page
When you want to generate simple link, you can use #Url.Action() like this:
<i class="glyphicon glyphicon-arrow-left"></i> Previous page
But when you want to generate ajax links, it's not so simple. Because #Ajax.ActionLink generates anchor with javascript or 'data-*' atributes that are handled by jquery-unobtrusive-ajax-min.js library.
So I wrote for my purpose extension methods to generate ActionLinks/RouteLinks in way you use #Html.BeginForm/#Ajax.BeginForm (surrounded by using).
Usage:
// instead
#Html.ActionLink("Previous page", "Index", "Customer", new { page = 1 })
// you can write
#using(Html.BeginActionLink("Index", "Customer", new { page = 1 }) {
<text><i class="glyphicon glyphicon-arrow-left"></i> Previous page</text>
}
// same with ajax links
#using(Ajax.BeginActionLink("Index", new { page = 1 }, new AjaxOptions { ... }) {
<text><i class="glyphicon glyphicon-arrow-left"></i> Previous page</text>
}
Methods BeginActionLink return instance of class MvcLink which implements IDisposable. In constructor it writes start tag and when disposed it writes end tag. Between curly brackets there is place for your code
namespace System.Web.Mvc
{
using System.Text.RegularExpressions;
public class MvcLink : IDisposable
{
internal static readonly string InnerText = "___F7ED35E0097945398D5A969F8DE2C63C___";
private static readonly Regex RegexPattern = new Regex(#"^\s*(?<startTag>.*)\s*" + InnerText + #"\s*(?<endTag>.*)\s*$", RegexOptions.Compiled | RegexOptions.Singleline);
private readonly ViewContext _viewContext;
private readonly string _endTag;
internal MvcLink(ViewContext viewContext, IHtmlString actionLink) {
_viewContext = viewContext;
var match = RegexPattern.Match(actionLink.ToHtmlString());
if (match.Success) {
var startTag = match.Groups["startTag"].Value;
_endTag = match.Groups["endTag"].Value;
_viewContext.Writer.Write(startTag);
}
}
public void Dispose() {
_viewContext.Writer.Write(_endTag);
}
}
}
Then it's up to you to write extension methods for HtmlHelper and AjaxHelper. There are too many overloads for method ActionLink/RouteLink so I prepared just those that I realy use in my application.
But it's easy to write others. You can see that I create instance of MvcLink, it takes ViewContext as first parameter and the result of builtin ActionLink with predefined InnerText that will be replaced by your content.
namespace System.Web.Mvc
{
using System.Web.Mvc.Ajax;
using System.Web.Mvc.Html;
public static class MvcHelperExtensions
{
public static MvcLink BeginActionLink(this AjaxHelper ajaxHelper, string actionName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) {
return new MvcLink(ajaxHelper.ViewContext, ajaxHelper.ActionLink(MvcLink.InnerText, actionName, routeValues, ajaxOptions, htmlAttributes));
}
public static MvcLink BeginActionLink(this HtmlHelper htmlHelper, string actionName, object routeValues, object htmlAttributes) {
return new MvcLink(htmlHelper.ViewContext, htmlHelper.ActionLink(MvcLink.InnerText, actionName, routeValues, htmlAttributes));
}
}
}

Nested objects in view model confuses MVC 4 routing?

So I have this view and it's generated by CMS. The brains of the view are in another view rendered by RenderAction.
Up until now it has looked like this:
Main.cshtml
<div>CMS content</div>
#{
var myContent = new MainContentModel() {
SomethingUsedFurtherDown = "Easier for CMS people to edit"
}
Html.RenderAction("_Main", "Foo", new { arg1 = "this", arg2 = "that", content = myContent });
}
MainContentModel.cs
namespace MyApp.Services.ViewModels.Foo
{
public class MainContentModel
{
public string SomethingUsedFurtherDown { get; set; }
}
}
MainViewModel.cs
namespace MyApp.Services.ViewModels.Foo
{
public class MainViewModel
{
public string Arg1 { set; set; }
public string Arg2 { set; set; }
public string Name { get; set; }
public string Age { get; set; }
public string Address { get; set; }
public MainContentModel Content { get; set; }
}
}
_Main.cshtml
#model MyApp.Services.ViewModels.Foo.MainViewModel
#Html.EditorFor(m => m.Name)
#Html.EditorFor(m => m.Age)
#Html.EditorFor(m => m.Address)
<!-- whatever - doesn't matter -->
FooController.cs
namespace My.App.Controllers
{
using System;
using System.Web.Mvc;
public class FooController
{
public ActionResult Main(string arg1, string arg2, MainContentModel cm)
{
var vm = new MainViewModel() { Arg1 = arg1, Arg2 = arg2, Content = cm };
return this.View("_Main", vm);
}
}
}
All works fine. So why am I bothering you? Well, this isn't the real code, obviously. There are many more arguments to the controller method and it's all getting rather messy. So I figured I would pass in the view model from the outer view.
Main.cshtml
<div>CMS content</div>
#{
var myVM = new MainViewModel() {
Arg1 = "this"
, Arg2 = "that"
, Content = new MainContentModel() {
SomethingUsedFurtherDown = "Easier for CMS people to edit"
}
};
Html.RenderAction("_Main", "Foo", new { vm = myVM });
}
...and just have one argument to the controller method
FooController.cs
namespace My.App.Controllers
{
using System;
using System.Web.Mvc;
public class FooController
{
public ActionResult Main(MainViewModel vm)
{
return this.View("_Main", vm);
}
}
}
And that's where the trouble starts. I get this error:
The view '_Main' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Foo/_Main.aspx
~/Views/Foo/_Main.ascx
~/Views/Shared/_Main.aspx
~/Views/Shared/_Main.ascx
~/Views/Foo/_Main.cshtml
~/Views/Foo/_Main.vbhtml
~/Views/Shared/_Main.cshtml
~/Views/Shared/_Main.vbhtml
~/Views/CMS/_Main.cshtml
~/Views/CMS/Foo/_Main.cshtml
But if I remove the initialisation of the content model, it all works again. Well, okay, it doesn't work work. But the error goes away, leaving me to come up with the obvious solution of adding the content model to the argument list and assigning it to the view model in the controller method.
Main.cshtml
<div>CMS content</div>
#{
var myVM = new MainViewModel() {
Arg1 = "this"
, Arg2 = "that"
};
var myCm = new MainContentModel() {
SomethingUsedFurtherDown = "Easier for CMS people to edit"
};
Html.RenderAction("_Main", "Foo", new { vm = myVM, cm = myCm });
}
FooController.cs
namespace My.App.Controllers
{
using System;
using System.Web.Mvc;
public class FooController
{
public ActionResult Main(MainViewModel vm, MainContentModel cm)
{
vm.Content = cm;
return this.View("_Main", vm);
}
}
}
Which is fine except that there are quite a few child objects of the real view model and I don't want to separate those out into arguments - it defeats the purpose of what is supposed to be a tidying up exercise.
Question
Is MVC4 supposed to be binding the child objects seamlessly and this is a bug? Or does it just not work this way and my only choice is to separate them out into extra parameters as above?
Cheers,
.pd.
Just in case anybody else has fallen into this trap. The problem was not the model binding. MVC 4 binds nested objects just fine. I was missing the subtlety of the error. The standard here is to prefix the "real" view name with an underscore and it was this that was not being found. The failure was occurring here:
return this.View("_Main");
What I didn't include in my post (cuz if I'd remembered, that would've fixed it!) was the fact that in the real situation the RenderAction looks like this:
RenderAction("Main", "Foo", new { area = "SomeArea", ... });
And I had missed off the area in my refactoring. Pretty embarrassing but if it stops somebody else beating their head against the wall then the shame will have been worth it :-)