I'm currently writing a grid for MVC that is an HtmlHelper extension and I'm running into a strange problem with it.
First of all, this is the code I'm using the construct the grid:
RenderedOutput = HtmlHelper.GridFor(Model)
.WithColumns(column =>
{
column.Bind(x => x.Name)
.WithCss("inline");
column.Bind(x => x.Age)
.WithCss("inline fixed right");
})
.Render();
Now, the entire fluent API is built up by using interfaces, to make the copy-past work a bit smaller, I do not post the interfaces here but I'll post the implementation:
First of all, the HtmlHelper:
public static IGridBuilder<TEntity> GridFor<TModel, TEntity>(this HtmlHelper<TModel> htmlHelper,
IEnumerable<TEntity> dataSource)
{
return new GridBuilder<TEntity>(htmlHelper, dataSource);
}
So this return a GridBuilder, of which the implementation is here:
#region Constructors
public GridBuilder(HtmlHelper helper, IEnumerable<TModel> data)
{
HtmlHelper = helper;
DataSource = data;
ColumnBuilders = new List<IColumnBuilder<TModel>>();
}
#endregion
#region Properties
private string Id { get; set; }
#endregion
#region IGridBuilder Members
public IList<IColumnBuilder<TModel>> ColumnBuilders { get; protected set; }
public IEnumerable<TModel> DataSource { get; private set; }
public HtmlHelper HtmlHelper { get; private set; }
public IGridBuilder<TModel> WithId(string id)
{
Id = id;
return this;
}
public IGridBuilder<TModel> WithColumns(Action<IColumnBuilder<TModel>> bindAllColumns)
{
bindAllColumns(new ColumnBuilder<TModel>(this));
return this;
}
public HtmlString Render()
{
var outputBuilder = new StringBuilder();
var headerMember = new TagBuilder("div");
headerMember.MergeAttribute("class", "gridHolder v-scroll");
if (!string.IsNullOrEmpty(Id))
{
headerMember.GenerateId(Id);
}
outputBuilder.AppendLine(headerMember.ToString(TagRenderMode.StartTag));
// Process all the available columns.
var rowBuilder = new TagBuilder("div");
rowBuilder.MergeAttribute("class", "row");
outputBuilder.AppendLine(rowBuilder.ToString(TagRenderMode.StartTag));
foreach (var column in ColumnBuilders)
{
var columnBuilder = new TagBuilder("div");
outputBuilder.AppendLine(columnBuilder.ToString(TagRenderMode.StartTag));
outputBuilder.AppendLine(columnBuilder.ToString(TagRenderMode.EndTag));
}
outputBuilder.AppendLine(rowBuilder.ToString(TagRenderMode.EndTag));
outputBuilder.AppendLine(headerMember.ToString(TagRenderMode.EndTag));
return new HtmlString(outputBuilder.ToString());
}
#endregion
}
The GridBuilder takes an Action of IColumnBuilder to build the column, so here it is:
public class ColumnBuilder<TModel> : IColumnBuilder<TModel>
{
#region Constructors
public ColumnBuilder(IGridBuilder<TModel> gridBuilder)
{
GridBuilderReference = gridBuilder;
}
#endregion
#region IColumnBuilder Members
public IGridBuilder<TModel> GridBuilderReference { get; private set; }
public string CssClass { get; private set; }
public IColumnBuilder<TModel> Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector)
{
// Reset the properties. This is needed because they are not cleared automatticly. It's not a new instance which is created.
CssClass = null;
GridBuilderReference.ColumnBuilders.Add(this);
return this;
}
public IColumnBuilder<TModel> WithCss(string className)
{
CssClass = className;
return this;
}
#endregion
}
First of all, it's my first fluent interface implementation so if it's not the good approach please point me in the right direction.
The situation:
When I'm constructing the grid, I pass in an action to bind the columns and those columns are fluent again as well, (I can add a class on a column for example). Now, to render those, I tought it was a good idea to hold a reference to all IColumnBuilder instances in my IGridBuilder, so that in my GridBuilder renders method I can do something like
// Rendering before the columns.
foreach (var c in myColumns) { c.Render(); }
// Rendering after the columns.
Therefore, I've created a list in the GridBuilder that will contain all the ColumnBuilders. When I execute a WithColumns, my GridBuilder object (this) is passed, and then in ColumnBuilder, on each Bind() function I add the ColumnBuilder object to the passed reference.
But this has a strange behaviour (for example, the list containing the ColumnBuilders, does all match the properties of the last ColumnBuilder executed).
Method 1
public class ColumnBuilderFactory<TModel> : IColumnBuilderFactory<TModel>
{
#region Constructors
public ColumnBuilderFactory(IGridBuilder<TModel> gridBuilder)
{
gridBuilderReference = gridBuilder;
}
#endregion
#region IColumnBuilderFactory Members
private IGridBuilder<TModel> gridBuilderReference { get; private set; }
internal IList<IColumnBuilder<TModel>> Columns {get; private set; }
public IColumnBuilder<TModel> New()
{
var column = new ColumnBuilder(gridBuilderReference);
Columns.Add(column);
return column;
}
#endregion
}
and GridBuilder WithColumns
becomes
public IGridBuilder<TModel> WithColumns(Action<IColumnBuilderFactory<TModel>> bindAllColumns)
{
var factory = new ColumnBuilderFactory<TModel>(this);
bindAllColumns(factory);
foreach(var column in factory)
{
this.ColumnBuilders.Add(column );
}
return this;
}
This has the usage
RenderedOutput = HtmlHelper.GridFor(Model)
.WithColumns(columnFactory =>
{
columnFactory.New().Bind(x => x.Name)
.WithCss("inline");
columnFactory.New().Bind(x => x.Age)
.WithCss("inline fixed right");
})
.Render();
Method 2
The alternative does much the same by cheating and ignoring this (using the FluentAPI to replace it), basically it's the same as your current usage but Bind
becomes
public IColumnBuilder<TModel> Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector)
{
var builder = new ColumnBuilder<TModel>(GridBuilderReference);
GridBuilderReference.ColumnBuilders.Add(builder);
return builder;
}
It relies on the GC tidying up the first(useless) instance
so.....
RenderedOutput = HtmlHelper.GridFor(Model)
.WithColumns(column =>
{
//column at this point isn't used, it's only there to avoid a NullReferenceException on the first call to Bind
column
.Bind(x => x.Name)
//Bind has returned a new ColumnBuilder to play with so the next call will be on the new instance
.WithCss("inline");
//Again this is just so we can make the next bind call
column
//Again bind replaces the previous instance with a new one so we won't overwrite Name with this call
.Bind(x => x.Age)
//This now sets the css on the new ColumnBuilder we just created for Age
.WithCss("inline fixed right");
})
.Render();
Example Console App showing Method 2
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace ConsoleApplication3
{
class Program
{
static void Main(string[] args)
{
var master = FluentHelper.GridFor(new Model())
.WithColumns(column =>
{
column.Bind(x => x.Name)
.WithCSS("inline");
column.Bind(x => x.Age)
.WithCSS("inline fixed right");
});
foreach(var c in master.children)
{
Console.WriteLine(c.Binding.ToString());
Console.WriteLine(c.CSS);
}
Console.ReadKey(true);
}
}
class Model
{
public string Name { get; set; }
public string Age { get; set; }
}
class GridBuilder<T>
{
public GridBuilder<T> WithColumns(Action<ColumnBuilder<T>> bindAllColumns)
{
bindAllColumns(new ColumnBuilder<T>(this));
return this;
}
public List<ColumnBuilder<T>> children = new List<ColumnBuilder<T>>();
}
class ColumnBuilder<T>
{
private GridBuilder<T> grid;
public string Binding;
public string CSS;
public ColumnBuilder(GridBuilder<T> grid)
{
// TODO: Complete member initialization
this.grid = grid;
}
public void WithCSS(string css)
{
this.CSS = css;
}
public ColumnBuilder<T> Bind<TItem>(Expression<Func<T, TItem>> propertySelector)
{
var builder = new ColumnBuilder<T>(grid);
builder.Binding = (propertySelector.Body as MemberExpression).Member.Name;
grid.children.Add(builder);
return builder;
}
}
static class FluentHelper
{
internal static GridBuilder<T> GridFor<T>(T model)
{
return new GridBuilder<T>();
}
}
}