Reputation: 32428
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:
// 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);
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?
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..
Upvotes: 7
Views: 2376
Reputation: 71
You can take advantage of your out of the box Metadata to provide you the property Id etc..
And then in your Extensions:
public static MvcHtmlString FocusFieldFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var fullPropertyName = html.ViewData.TemplateInfo.GetFullHtmlFieldId(metadata.PropertyName);
var jsonData =
@"<script type=""text/javascript"">
$(document).ready(function() {
setTimeout(function() {
$('#" + fullPropertyName + @"').focus();
}, 500);
});
</script>";
return MvcHtmlString.Create(jsonData);
}
Upvotes: 1
Reputation: 18349
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);
Upvotes: 5
Reputation: 1038710
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.
Upvotes: 2
Reputation: 359
Maybe I'm missing something, but since you're passing in the field name to your FocusOnField function, could you not also just pass the string of the field name as well which will be the default ID in the view then set a ViewData value? Then your javascript could do something like...
<script type="text/javascript">
onload = focusonme;
function focusonme() {
var element = document.getElementById(@ViewData["FieldToFocus"]);
element.focus();
}
</script>
Maybe this isn't exactly what you're after, but the JS will definitely work this way...
Upvotes: -1