5

I had a somewhat crazy idea earlier tonight, and got 3/4 of the way done implementing it and have run in to a weird problem.. I wanted to automatically generate an index of all methods on a controller than return an ActionResult, as well as a simple form for each to sumbmit their valid data.. Seemed like a pretty simple thing to do via reflection:

Quickie ViewModel to hold each reflected action:

public class ReflectedAction
{
    public ReflectedAction(MethodInfo methodInfo, string controllerName)
    {
        this.ActionName = methodInfo.Name;
        this.ControllerName = controllerName;
        this.Parameters = methodInfo.GetParameters().Select(p => p.Name);
    }

    public string ControllerName { get; set; }

    public string ActionName { get; set; }

    public IEnumerable<string> Parameters { get; set; }
}

Action to reflect all the actions on the current controller:

public virtual ActionResult AutoIndex()
{
    Type controllerType = this.ControllerContext.Controller.GetType();
    string controllerName = controllerType.Name.Replace("Controller", string.Empty);

    var methods = this.ControllerContext.Controller.GetType().GetMethods().Where(
            m => m.ReturnType.Name.Contains("ActionResult"));

    var model = methods.Select(m => new ReflectedAction(m, controllerName));

    return View(model);
}

Inside the view, I just wanted to use a simple WebGrid to render out each action as a row, with the first column being the name of the action, and the second column being a mini-form, with the ability to fill out any fields that the action has (I tried doing it as a helper, or inline in the grid format, latter is include here:

@using TfsMvc.Controllers
@model IEnumerable<TestController.ReflectedAction>

@{
    ViewBag.Title = "AutoIndex";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>AutoIndex</h2>

@{
    var grid = new WebGrid(
        source: Model,
        ajaxUpdateContainerId: "grid",
        defaultSort: "ActionName",
        canPage: false);
}

<div id="grid">
    @grid.GetHtml(
    tableStyle: "grid",
    headerStyle: "head",
    alternatingRowStyle: "alt",
    columns: grid.Columns(
        grid.Column("ActionName"),
        grid.Column(format: (action) =>
            {
                using (Html.BeginForm((string)action.ActionName, (string)action.ControllerName, FormMethod.Get))
                {
                    string htmlString = string.Empty;

                    foreach (string parameter in action.Parameters)
                    {
                        htmlString = "<span>" + Html.Label(parameter) + Html.TextBox(parameter) + "</span>";
                    }

                    htmlString += "<input type=\"submit\" />";

                    return new HtmlString(htmlString);
                }
            }))
        )
</div>

The grid appears to render correctly, but the weird part is that all the form html tags render outside the grid, but the controls render inside the grid:

<div id="grid">
    <form action="/Test/CloneTestPlan" method="get"></form>
    <form action="/Test/ConfigureTestPlan" method="get"></form>
    <form action="/Test/EnvConfig" method="get"></form>
    <form action="/Test/FixTestLink" method="get"></form>

    <!-- ton of other actions snipped-->

    <table class="grid">
        <thead>
            <tr class="head"><th scope="col"><a href="#" onclick="$(&#39;#grid&#39;).load(&#39;/Test/SecretIndex?sort=ActionName&amp;sortdir=DESC&amp;__=634581349851993336 #grid&#39;);">ActionName</a></th><th scope="col"></th></tr>
        </thead>
        <tbody>
            <tr><td>CloneTestPlan</td><td><span><label for="subid">subid</label><input id="subid" name="subid" type="text" value="" /></span><input type="submit" /></td></tr>
            <tr class="alt"><td>ConfigureTestPlan</td><td><span><label for="apply">apply</label><input id="apply" name="apply" type="text" value="" /></span><input type="submit" /></td></tr>
            <tr><td>EnvConfig</td><td><span><label for="create">create</label><input id="create" name="create" type="text" value="" /></span><input type="submit" /></td></tr>
            <tr class="alt"><td>FixTestLink</td><td><span><label for="commit">commit</label><input id="commit" name="commit" type="text" value="" /></span><input type="submit" /></td></tr>

            <!-- ton of other actions snipped-->

        </tbody></table>
</div>

As you can see, the tags render outside of the table! Any idea what I'm doing wrong here? Or can you just not do BeginForm inside a Webgrid? Any better approach for making a bunch of individual forms?

Thanks in advance!

superlime
  • 906
  • 8
  • 10
  • This is really interesting. What do you do with this? Do you use it to demonstrate the functionality to your dev team? – Brian White Jun 22 '12 at 20:05
  • Sorry for the late reply, didn't see you'd added a comment. It's actually mainly me being lazy and not manually making a static index. :) I had a large number of actions I'd made that I couldn't necessarily remember the name of, or the exact parameters they needed. They were generally sanely named, so I could usually guess...but having an autogenerated index that showed them all was much cleaner. – superlime Sep 10 '13 at 19:20

2 Answers2

3

Try rendering the <form> yourself without using the helper.

It looks like the lambdas are executed inside helper before it spits out the content, which causes the BeginForm to render to output instantly.

Jakub Konecki
  • 45,581
  • 7
  • 87
  • 126
  • Aha! That'd at least explain the bizarre behavior! Rendering
    by hand does *does* work, but it just feels dirty and means that I have to worry about properly rendering the path by hand too. I'd really like to have some sort of cleaner fashion to do this.. Any idea if there's a way to force the helper to execute within the lambda with the timing that I'm expecting? This seems like a bug within MVC, if not..
    – superlime Nov 30 '11 at 03:17
  • You can still use `UrlHelper` to render the url. You can also try to call `Html.BeginForm` and `Html.EndForm` without using `using` directive. Or come up with own extension methods. – Jakub Konecki Nov 30 '11 at 09:19
2

I'm not sure if it will help in your case, but I had similar problem and what I did was:

  1. Create partial with following code

    @using(Html.BeginForm()){
        //some code
    }
    
  2. In place where problem occurred, call this partial, so in your case it would be something like:

    grid.Column(format: (action) =>
    {
        Html.Partial("SomePartial")
    })
    

PS: The place where I called Partial was different so I'm not sure if above will work.

Mihai Iorga
  • 39,330
  • 16
  • 106
  • 107
kbalcerek
  • 41
  • 5