Search code examples
c#asp.net-mvclambdachaining

Chaining lambda expressions in MVC.Net without repetitious passing of HtmlHelper object


My goal is to create an object to allow chaining of commands in MVC.Net views.

Here is an example use in a view of a menu I created using this concept:

<nav class="navigation">
    <%: Html
        .menu()
            .item("Introduction", "Introduction", "Home")
            .item("About", "About", "Home")
            .item("Systems", "Index", "Systems")
            /*.item("Categories", "Categories", "Health")*/
            .item("Test Cases", "TestCases", "Testing")
            .category("Logging")
                .item("UniMon Events", "UniMonEvents", "Logging")
            .end()
        .end() %>
</nav>

As you can see it allows for the quick construction of a multi-tiered menu with interdependencies between the various parts.

I would like to achieve this same effect for a form using lambda expressions.

The ideal syntax would look like this:

<%: Html
    .form()
        .hidden(m=>m.property1)
        .hidden(m=>m.property2)
    .end() %>

Where I am running into trouble is with the hidden method. It seems there is no way to get the compiler to infer m without passing it to the method hidden.

I can achieve this syntax:

<%: Html
    .form()
        .hidden(Html, m=>m.property1)
        .hidden(Html, m=>m.property2)
    .end() %>

Using this class and an extension method(not shown):

public class RouteForm
{
    public HtmlHelper HtmlHelper { get; private set; }
    public Dictionary<string, string> PostData { get; private set; }

    public RouteForm(HtmlHelper htmlHelper)
    {
        HtmlHelper = htmlHelper;
        PostData = new Dictionary<string, string>();
    }

    public RouteForm hidden<TModel, TValue>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
    {
        string name = ExpressionHelper.GetExpressionText(expression);
        string value = GetFieldValue(htmlHelper, expression);
        PostData.Add(name, value);
        return this;
    }
    private static string GetFieldValue<TModel, TValue>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
    {
        object oValue = expression.Compile()(htmlHelper.ViewData.Model);
        string value = (oValue is Enum) ? ((int)oValue).ToString() : oValue.ToString();
        return value; ;
    }
    public MvcHtmlString end()
    {
        //TODO: render form with post data
        return MvcHtmlString.Empty;
    }
}

I thought that perhaps a class with a generic type might be what I am looking for, so I tried this:

public class RouteForm<TModel>
{
    public HtmlHelper<TModel> HtmlHelper { get; private set; }
    public Dictionary<string, string> PostData { get; private set; }

    public RouteForm(HtmlHelper<TModel> htmlHelper)
    {
        HtmlHelper = htmlHelper;
        PostData = new Dictionary<string, string>();
    }

    public RouteForm<TModel> hidden<TModel, TValue>(Expression<Func<TModel, TValue>> expression)
    {
        string name = ExpressionHelper.GetExpressionText(expression);
        string value = GetFieldValue(expression);
        PostData.Add(name, value);
        return this;//ERRORS: TModel is TModel
    }
    private string GetFieldValue<TModel, TValue>(Expression<Func<TModel, TValue>> expression)
    {
        object oValue = expression.Compile()(
            (TModel)HtmlHelper.ViewData.Model //ERRORS: Cannot convert type TModel to TModel
        );
        string value = (oValue is Enum) ? ((int)oValue).ToString() : oValue.ToString();
        return value; ;
    }
    public MvcHtmlString end()
    {
        //TODO: render form with post data
        return MvcHtmlString.Empty;
    }
}

I put the errors in the code above using comments.

Thanks!


Solution

  • You're using too many generic parameters.

    Methods like GetFieldValue<TModel, ...> create a second TModel paramter which is not related to the first one.

    In other words, they allow you to write

    new RouteForm<PersonModel>().GetFieldValue<TruckModel, ...>()
    

    This is obviously wrong.

    Instead, just get rid of that parameter from each method and let them use the class' TModel parameter instead.