Reputation: 337560
I am attempting to genericise a complex control which is used in my website quite often but with different fields. The functionality in the control is always the same, it's just the underlying fields which change.
To achieve the method of showing different fields I am attempting to create a HTMLHelper extension which accepts a Expression<Func<TModel,TProperty>>
as a parameter, which would contain the properties of a class required for display in the control. For example:
The view:
@model Project.Core.Page
@Html.MyHelper(p => new { p.Author.Name, p.Author.Location, p.Author.Age });
It's the extension I'm having problems with - how can I iterate over the provided params in the lambda to provide each with a TextBoxFor()
, or even manually create a input
element and populate it with the value
and name
of the lambda parameter?
The extention in psuedo:
public static MvcHtmlString MyHelper<TModel,TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel,TProperty>> expression) {
foreach (var parameter in expression.???) {
// helper.TextBoxFor(???)
// TagBuilder("input").Attributes("name", expression.???)
}
}
I feel like I've been staring at this for far too long, and I'm also feeling there's a more simple way I'm overlooking of achieving this.
Any help is greatly appreciated. If you need further details, or I've missed something important, let me know.
Upvotes: 8
Views: 2635
Reputation: 7592
If you assume the following:
MemberExpressions
, and do not contain a call to a method on the model or on its childrenThen you can achieve what you want by using the following approach:
Edit:
After realizing that my first example could not handle objects with complex properties, I updated the code to use a helper method to access property values. This method traverses over the property chain using recursion to return the appropriate values.
public static MvcHtmlString MyHelper<TModel,object>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel,object>> expression) {
var newExpression = expression.Body as NewExpression;
TModel model = helper.ViewData.Model;
foreach (MemberExpression a in newExpression.Arguments) {
var propertyName = a.Member.Name;
var propertyValue = GetPropertyValue<TModel>(model, a);
// Do whatever you need to with the property name and value;
}
}
private static object GetPropertyValue<T>(T instance, MemberExpression me) {
object target;
if (me.Expression.NodeType == ExpressionType.Parameter) {
// If the current MemberExpression is at the root object, set that as the target.
target = instance;
}
else {
target = GetPropertyValue<T>(instance, me.Expression as MemberExpression);
}
// Return the value from current MemberExpression against the current target
return target.GetType().GetProperty(me.Member.Name).GetValue(target, null);
}
Note: I did not implement this directly as a MVC extension method in my IDE, so a slight variation of the syntax may be required.
Upvotes: 2
Reputation: 1982
Consider this:
Create App_Code folder
Put the razor helper file Templates.cshtml.
It looks like below:
@helper Print(string myvalue,string special="")
{
<pre> <input id="id" type="text" value ="@myvalue" data-val="@special"/> </pre>
}
This way you dont have to write HTML in C# files. It is very handy.
Authors.cshtml looks like below:
@model IEnumerable<MvcApplication1.Models.Author>
@{
ViewBag.Title = "Authors";
}
<h2>Authors</h2>
@foreach(var auth in Model)
{
@Templates.Print(auth.Name);
}
books.cshtml looks like below:
@model IEnumerable<MvcApplication1.Models.Book>
@{
ViewBag.Title = "Books";
}
<h2>Books</h2>
@foreach(var book in Model)
{
@Templates.Print(book.Title,book.ISBN);
}
Plugin all the special properties as you need per model class. If it gets too complicated, look into dynamic and expando object. It depends on how complex your models/viewmodels are.
Upvotes: 1
Reputation: 6103
Perhaps a builder-style API might simplify things:
@(Html.MyHelper(p)
.Add(p => p.Author.Age)
.Add(p => p.Author.LastName, "Last Name")
.Build())
Notice that this allows you to add optional params in case you need them.
The code would look something like this
public static class Test
{
public static Helper<TModel> MyHelper<TModel>(this HtmlHelper helper, TModel model)
{
return new Helper<TModel>(helper, model);
}
}
public class Helper<TModel>
{
private readonly HtmlHelper helper;
private readonly TModel model;
public Helper(HtmlHelper helper, TModel model)
{
this.helper = helper;
this.model = model;
}
public Helper<TModel> Add<TProperty>(Expression<Func<TModel, TProperty>> expression)
{
// TODO
return this;
}
public MvcHtmlString Build()
{
return new MvcHtmlString("TODO");
}
}
Upvotes: 1
Reputation: 1500505
The expression you'll have created will be relatively complicated - it'll need to fetch all the properties and then call the anonymous type constructor. "Disassembling" that may get painful... although if you still want to try, I'd suggest just leaving an empty method implementation and looking in the debugger to see what the expression looks like.
If you'd be happy to settle for a slightly uglier form of calling code, it would be much simpler to implement this:
@Html.MyHelper(p => p.Author.Name, p => p.Author.Location, p => p.Author.Age);
You could make that take a params Expression<TModel, object>
or you could declare multiple overloads with different numbers of parameters, e.g.
// Overload for one property
MyHelper<TModel, TProperty1>(this ..., Expression<Func<TModel, TProperty1>> func1)
// Overload for two properties
MyHelper<TModel, TProperty1, TProperty2>(this ...,
Expression<Func<TModel, TProperty1>> func1,
Expression<Func<TModel, TProperty2>> func2)
etc.
Upvotes: 2