Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing model property expression to view

I want to have a way of getting a view to focus on a particular model property from a controller in a generic way.

What i have so far is:


Controller:

// To become an extension/base class method
private void FocusOnField<TModel, TProperty>(Expression<Func<TModel, TProperty>> fieldExpression)
{
    ViewData["FieldToFocus"] = fieldExpression;
}

...

FocusOnField((ConcreteModelClass m) => m.MyProperty);

View

    public static class ViewPageExtensions
    {
        public static MvcHtmlString FocusScript<TModel>(this ViewPage<TModel> viewPage)
        {
            if (viewPage.ViewData["FieldToFocus"] != null)
            {
                return MvcHtmlString.Create(
@"<script type=""text/javascript"" language=""javascript"">
    $(document).ready(function() {
        setTimeout(function() {
            $('#" + viewPage.Html.IdFor((System.Linq.Expressions.Expression<Func<TModel, object>>)viewPage.ViewData["FieldToFocus"]) + @"').focus();
        }, 500);
    });
</script>");
            }
            else
            {
                return MvcHtmlString.Empty;
            }
        }
    }

The problem I'm faced with now, is that in the view's FocusScript method I don't know the return type of the property to focus on, and casting to (System.Linq.Expressions.Expression<Func<TModel, object>>) fails for any property that doesn't return an object.

I can't just add a second generic parameter for the property because I don't know what the return type of the property the controller wants me to focus on is.

How can I write my view extension method FocusScript in a generic way so it can be used with properties of varying return types?


Why is there a problem?

I know I could just pass the Id of the control i want to focus on in the controller and have javascript read that id, find the control and focus on it. However, I don't like having something that belongs in the view (the Id of a control) hard-coded in the controller. I want to tell the method what property I want and it should know the Id to use in the same way the view normally gets/creates the Id for a control.

Lets say I have a model:

class MyModel
{
    public int IntProperty { get; set; }
    public string StringProperty { get; set; }
}

In different places in the controller I want to focus one different fields:

FocusOnField((MyModel m) => m.IntProperty);
...
FocusOnField((MyModel m) => m.StringProperty);

Now, in the first case the expression is a function returning an integer, in the second case it's returning a string. As a result I don't know what to cast my ViewData["FieldToFocus"] to (to pass it to IdFor<>()) as it varies based on the property..

like image 551
George Duckett Avatar asked Jul 12 '12 11:07

George Duckett


People also ask

What is@ model in Cshtml?

This @model directive allows you to access the movie that the controller passed to the view by using a Model object that's strongly typed. For example, in the Details. cshtml template, the code passes each movie field to the DisplayNameFor and DisplayFor HTML Helpers with the strongly typed Model object.

What is Property expression C#?

Property(Expression, PropertyInfo)Creates a MemberExpression that represents accessing a property. public: static System::Linq::Expressions::MemberExpression ^ Property(System::Linq::Expressions::Expression ^ expression, System::Reflection::PropertyInfo ^ property); C# Copy. public static System.Linq.Expressions.

How to Create model in c# Mvc?

In the MVC application in Visual Studio, and right-click on the Model folder, select Add -> and click on Class... It will open the Add New Item dialog box. In the Add New Item dialog box, enter the class name Student and click Add. This will add a new Student class in model folder.


2 Answers

I think I've come up with a solution to your problem - it works in my environment but then I've had to guess how your code probably looks.

public static class ViewPageExtensions
{
    public static MvcHtmlString GetIdFor<TViewModel, TProperty>(ViewPage<TViewModel> viewPage, Expression<Func<TViewModel, TProperty>> expression)
    {
        return viewPage.Html.IdFor(expression);
    }

    public static MvcHtmlString FocusScript<TViewModel>(this ViewPage<TViewModel> viewPage)
    {
        if (viewPage.ViewData["FieldToFocus"] == null)
            return MvcHtmlString.Empty;

        object expression = viewPage.ViewData["FieldToFocus"];

        Type expressionType = expression.GetType(); // expressionType = Expression<Func<TViewModel, TProperty>>
        Type functionType = expressionType.GetGenericArguments()[0]; // functionType = Func<TViewModel, TProperty>
        Type[] functionGenericArguments = functionType.GetGenericArguments(); // functionGenericArguments = [TViewModel, TProperty]
        System.Reflection.MethodInfo method = typeof(ViewPageExtensions).GetMethod("GetIdFor").MakeGenericMethod(functionGenericArguments); // method = GetIdFor<TViewModel, TProperty>

        MvcHtmlString id = (MvcHtmlString)method.Invoke(null, new[] { viewPage, expression }); // Call GetIdFor<TViewModel, TProperty>(viewPage, expression);

        return MvcHtmlString.Create(
            @"<script type=""text/javascript"" language=""javascript"">
                $(document).ready(function() {
                    setTimeout(function() {
                        $('#" + id + @"').focus();
                    }, 500);
                });
            </script>");
    }
}

Perhaps there's a more elegant way to do, but I think what it boils down to is you're trying to cast an object (ie return type of ViewData["FieldToFocus"]) to the correct expression tree Expression<Func<TViewModel, TProperty>> but like you've said you don't know what TProperty should be.

Also to make this work I had to add another static method to GetIdFor because at the moment I'm not too sure how to invoke an extension method. Its just a wrapper to call the IdFor extension method.

You could make it less verbose (but probably less readable).

object expression = viewPage.ViewData["FieldToFocus"];

MethodInfo method = typeof(ViewPageExtensions).GetMethod("GetIdFor")
    .MakeGenericMethod(expression.GetType().GetGenericArguments()[0].GetGenericArguments());

MvcHtmlString id = (MvcHtmlString)method.Invoke(null, new[] { viewPage, expression });

One final thought, would the output of HtmlHelper.IdFor ever differ from ExpressionHelper.GetExpressionText? I don't fully understand IdFor, and wonder if it will always give you a string that matches the property name.

ViewData["FieldToFocus"] = ExpressionHelper.GetExpressionText(fieldExpression);
like image 191
Chris Moutray Avatar answered Sep 18 '22 22:09

Chris Moutray


It's as simple as using a LambdaExpression. No need to go the Expression<Func<TModel, TProperty>> way.

public static class HtmlExtensions
{
    public static string IdFor(
        this HtmlHelper htmlHelper, 
        LambdaExpression expression
    )
    {
        var id = ExpressionHelper.GetExpressionText(expression);
        return htmlHelper.ViewData.TemplateInfo.GetFullHtmlFieldId(id);
    }

    public static MvcHtmlString FocusScript(
        this HtmlHelper htmlHelper
    )
    {
        if (htmlHelper.ViewData["FieldToFocus"] != null)
        {
            return MvcHtmlString.Create(
            @"<script type=""text/javascript"">
                $(document).ready(function() {
                    setTimeout(function() {
                        $('#" + htmlHelper.IdFor((LambdaExpression)htmlHelper.ViewData["FieldToFocus"]) + @"').focus();
                    }, 500);
                });
            </script>");
        }
        else
        {
            return MvcHtmlString.Empty;
        }
    }
}

then in your controller:

public ActionResult Index()
{
    FocusOnField((MyModel m) => m.IntProperty);
    return View(new MyModel());
}

and in your view:

@model MyModel

@Html.FocusScript()

This being said I am leaving without comment the fact that a controller action is setting a focus.

like image 22
Darin Dimitrov Avatar answered Sep 21 '22 22:09

Darin Dimitrov