Complexity
Complexity

Reputation: 5820

C# Fluent API: How to construct

Currently, I'm creating custom MVC Html Helpers which I will be using through a fluent API. To give an example to understand it, I'll have the following helper which is (or should be in a not too distant future) generate a grid:

@(Html.GridFor(Model)
  .WithName("MyName")
  .WithColumns(model =>
  {
      model.Bind(x => x.Name);
      model.Bind(x => x.DateCreated);
      model.Bind(x => x.DateUpdated);
  }).Render());

Now, everything is constructed with the start point. The IGridBuilder.

/// <summary>
///     When implemented by a class, it defines the class as an object that can construct a grid by using a fluent API.
/// </summary>
public interface IGridBuilder<TModel> : IHtmlHelper, IDataSource<TModel>
{
    #region Properties

    /// <summary>
    ///     Gets the name of the <see cref="IGridBuilder{TModel}" />.
    /// </summary>
    string Name { get; }

    #endregion

    #region Methods

    /// <summary>
    ///     Sets the name of the <see cref="IGridBuilder{TModel}" />. This name will be used as an id on the outer element that
    ///     holds the entire grid.
    /// </summary>
    /// <param name="name">The name that the <see cref="IGridBuilder{TModel}" /> should have.</param>
    /// <returns>An <see cref="IGridBuilder{TModel}" /> that can be used to construct the grid through a fluent API.</returns>
    IGridBuilder<TModel> WithName(string name);

    /// <summary>
    ///     Set the columns of the model that should be bound to grid.
    /// </summary>
    /// <param name="bindAllColumns">The action that will bind all the columns.</param>
    /// <returns>An <see cref="IGridBuilder{TModel}" /> that is used to construct the grid.</returns>
    IGridBuilder<TModel> WithColumns(Action<IColumnBinder<TModel>> bindAllColumns);

    /// <summary>
    ///     Renders the grid with all the set properties.
    /// </summary>
    /// <returns>A <see cref="MvcHtmlString" /> that contains the HTML representation of the grid.</returns>
    MvcHtmlString Render();

    #endregion
}

and to make the bind commands, I'm using an IColumnBinder interface:

/// <summary>
/// When implemented by a class, this class is marked as being an builder that can construct a column through a fluent API.
/// </summary>
/// <typeparam name="TModel"></typeparam>
public interface IColumnBinder<TModel> : IHtmlHelper, IDataSource<TModel>
{
    #region Methods

    /// <summary>
    ///     Binds an column to the grid.
    /// </summary>
    /// <typeparam name="TItem">The type of the column on which to bind the items.</typeparam>
    /// <param name="propertySelector">The functional that will bind the control to the grid.</param>
    void Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector);

/// <summary>
///     Apply a specific css class on an element.
/// </summary>
/// <param name="className">The name of the css class that should be placed on the element.</param>
/// <returns>As <see cref="IColumnBinder{TModel}"/> that is used to construct this column through a fluent API.</returns>
IColumnBinder<TModel> WithCss(string className);

    #endregion
}

Now, what's the best approach to link an IColumnBuilder to an IGridBuilder?

To make it very short, I'm struggling with the following:

The IColumnBuilder set ups specific properties but the rendering takes place in the IGridBuilder interface.

The main problem lies in the following code:

/// <summary>
///     Set the columns of the model that should be bound to grid.
/// </summary>
/// <param name="bindAllColumns">The action that will bind all the columns.</param>
/// <returns>An <see cref="IGridBuilder{TModel}" /> that is used to construct the grid.</returns>
public IGridBuilder<TModel> WithColumns(Action<IColumnBinder<TModel>> bindAllColumns)
{
    bindAllColumns(new ColumnBinder<TModel>());

    return this;
}

So here I execute the action to bind the column:

model.Bind(x => x.Name)

But how can I keep a reference between the IGridBuilder and the IColumnBuilder to construct it in a proper way afterwars?

Or are there other solutions?

Upvotes: 3

Views: 1029

Answers (1)

Complexity
Complexity

Reputation: 5820

Ok,

After hours of searching I've found a solution and therefore I'm answering my own question. However, if there are users that have another approach to the same problem, please tell me so I can adapt me code maybe.

This code will primarly use a class to transfer objects, since a class is a reference type it can be passed to another object, and that object can manipulate the object.

So, I have written a custom HTML helper that should work as the following:

@(Html.GridFor(Model)
  .WithName("MyName")
  .WithColumns(model =>
  {
      model.Bind(x => x.Name).WithCss("row first");
      model.Bind(x => x.DateCreated);
      model.Bind(x => x.DateUpdated);
  }).Render());

So, I do have a model that's enumerable and that I will pass to the grid. The grid takes 3 columns of the model and renders the grid.

For this code, I have a couple of interfaces which helps me throughout the process:

  1. An HTML Helper interface (just holds an object to the HtmlHelper):

    /// <summary>
    ///     Provides a way to extend the <see cref="HtmlHelper" /> to construct objects of various kinds.
    /// </summary>
    public static class HtmlHelperExtensions
    {
        #region Grid
    
        /// <summary>
        ///     Constructs a grid for a property that holds a collection.
        /// </summary>
        /// <typeparam name="TModel">The type of the model on which this grid is being build.</typeparam>
        /// <typeparam name="TEntity">The type of a single item in the collection.    </typeparam>
        /// <param name="htmlHelper">The helper on which this method is executed.    </param>
        /// <param name="dataSource">The datasource on which the items are bound.    </param>
        /// <returns>An <see cref="IGridBuilder{TEntity}" /> that is used to construct the grid.</returns>
        public static IGridBuilder<TEntity> GridFor<TModel, TEntity>(this     HtmlHelper<TModel> htmlHelper,
            IEnumerable<TEntity> dataSource)
        {
            return new GridBuilder<TEntity>(htmlHelper, dataSource);
        }
    
        #endregion
    }
    
  2. A Datasource interface (just holds an interface to the datasource):

    public interface IDataSource<out TModel>
    {
        #region Properties
    
         /// <summary>
         ///     Gets the source that will be bound to the implemented object.
         /// </summary>
         IEnumerable<TModel> DataSource { get; }
    
         #endregion
    }
    

And then we have all the other code.

  1. The HTML Helper extension class is the first one:

    /// <summary>
    ///     Provides a way to extend the <see cref="HtmlHelper" /> to construct objects of various kinds.
    /// </summary>
    public static class HtmlHelperExtensions
    {
        #region Grid
    
        /// <summary>
        ///     Constructs a grid for a property that holds a collection.
        /// </summary>
        /// <typeparam name="TModel">The type of the model on which this grid is being build.</typeparam>
        /// <typeparam name="TEntity">The type of a single item in the collection.</typeparam>
        /// <param name="htmlHelper">The helper on which this method is executed.</param>
        /// <param name="dataSource">The datasource on which the items are bound.</param>
        /// <returns>An <see cref="IGridBuilder{TEntity}" /> that is used to construct the grid.</returns>
        public static IGridBuilder<TEntity> GridFor<TModel, TEntity>(this HtmlHelper<TModel> htmlHelper,
            IEnumerable<TEntity> dataSource)
        {
            return new GridBuilder<TEntity>(htmlHelper, dataSource);
        }
    
        #endregion
    }
    
  2. Then the next one is an implementation of the IGridBuilder interface:

    /// <summary>
    ///     Provides an implemention of the <see cref="IGridBuilder{TModel}" /> that is used to construct the grid through a
    ///     fluent API.
    /// </summary>
    /// <typeparam name="TModel">The type of the model that the grid will hold.</typeparam>
    public class GridBuilder<TModel> : IGridBuilder<TModel>
    {
        #region Constructors
    
        /// <summary>
        ///     Creates a new instance of the <see cref="GridBuilder{TModel}" />.
        /// </summary>
        /// <param name="helper">The <see cref="HtmlHelper" /> that is used to construct the grid.</param>
        /// <param name="dataSource">The collection of objects that will be bound to the grid.</param>
        public GridBuilder(HtmlHelper helper, IEnumerable<TModel> dataSource)
        {
            htmlHelper = helper;
            DataSource = dataSource;
            Constructor = new GridConstructor<TModel>(htmlHelper, DataSource);
        }
    
        #endregion
    
        #region IGridBuilder Members
    
        /// <summary>
        ///     Gets the name of the <see cref="IGridBuilder{TModel}" />.
        /// </summary>
        public string Name { get; private set; }
    
        /// <summary>
        ///     Gets the constructor that will be used to construct this <see cref="IGridBuilder{TModel}" />.
        /// </summary>
        public IGridContructor<TModel> Constructor { get; set; }
    
        /// <summary>
        ///     Gets the source that will be bound to the implemented object.
        /// </summary>
        public IEnumerable<TModel> DataSource { get; private set; }
    
        /// <summary>
        ///     Gets the <see cref="HtmlHelper" /> object.
        /// </summary>
        public HtmlHelper htmlHelper { get; private set; }
    
        /// <summary>
        ///     Sets the name of the <see cref="IGridBuilder{TModel}" />. This name will be used as an id on the outer element that
        ///     holds the entire grid.
        /// </summary>
        /// <param name="name">The name that the <see cref="IGridBuilder{TModel}" /> should have.</param>
        /// <returns>An <see cref="IGridBuilder{TModel}" /> that can be used to construct the grid through a fluent API.</returns>
        public IGridBuilder<TModel> WithName(string name)
        {
            Name = name;
            return this;
        }
    
        /// <summary>
        ///     Set the columns of the model that should be bound to grid.
        /// </summary>
        /// <param name="bindAllColumns">The action that will bind all the columns.</param>
        /// <returns>An <see cref="IGridBuilder{TModel}" /> that is used to construct the grid.</returns>
        public IGridBuilder<TModel> WithColumns(Action<IColumnBinder<TModel>> bindAllColumns)
        {
            var columnBinder = new ColumnBinder<TModel>(Constructor);
    
            bindAllColumns(columnBinder);
            return this;
        }
    
        /// <summary>
        ///     Renders the grid with all the set properties.
        /// </summary>
        /// <returns>A <see cref="MvcHtmlString" /> that contains the HTML representation of the grid.</returns>
        public MvcHtmlString Render()
        {
            var outputBuilder = new StringBuilder();
    
            BaseElementBuilder parentElement = DivFactory.DivElement().WithCss("header");
    
            outputBuilder.Append(parentElement.ToString(TagRenderMode.StartTag));
            outputBuilder.Append(parentElement.ToString(TagRenderMode.EndTag));
    
            return new MvcHtmlString(outputBuilder.ToString());
        }
    
        #endregion
    }
    
  3. Then an implementation of the IGridColumnBinder:

    /// <summary>
    ///     Provides an implementation of the <see cref="IColumnBinder{TModel}" /> that can be used to construct a column
    ///     through a fluent API.
    /// </summary>
    /// <typeparam name="TModel">The type of the datasource that's bound to the grid.</typeparam>
    public class ColumnBinder<TModel> : IColumnBinder<TModel>
    {
        #region Constructors
    
        /// <summary>
        ///     Creates a new instance of the <see cref="ColumnBinder{TModel}" />.
        /// </summary>
        /// <param name="constructor">An <see cref="IGridContructor{TModel}" /> that contains the builder to construct the grid.</param>
        public ColumnBinder(IGridContructor<TModel> constructor)
        {
            Constructor = constructor;
        }
    
        #endregion
    
        #region IColumnBinder Members
    
        /// <summary>
        ///     Gets the values that are bound to this <see cref="IColumnBinder{TModel}" />.
        /// </summary>
        public IGridContructor<TModel> Constructor { get; private set; }
    
        /// <summary>
        ///     Gets the css class of the <see cref="IColumnBinder{TModel}" />.
        /// </summary>
        public string CssClass { get; private set; }
    
        /// <summary>
        ///     Gets the values that are bound to this <see cref="IColumnBinder{TModel}" />.
        /// </summary>
        public IList<object> Values { get; set; }
    
        /// <summary>
        ///     Binds an column to the grid.
        /// </summary>
        /// <typeparam name="TItem">The type of the column on which to bind the items.</typeparam>
        /// <param name="propertySelector">The functional that will bind the control to the grid.</param>
        /// <returns>As <see cref="IColumnBinder{TModel}" /> that is used to construct this column through a fluent API.</returns>
        public IColumnBinder<TModel> Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector)
        {
            string name = ExpressionHelper.GetExpressionText(propertySelector);
            name = Constructor.htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
    
            ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => default(TModel),
                typeof (TModel), name);
    
            // Get's the name to display on the column in grid. The Display attribute is used if present, otherwise the name of the property is used.
            string displayName = string.IsNullOrEmpty(metadata.DisplayName)
                ? metadata.PropertyName
                : metadata.DisplayName;
    
            Values =
                Constructor.DataSource.Select(myVar => propertySelector.Compile()(myVar))
                    .Select(dummy => (object) dummy)
                    .ToList();
    
            Constructor.builderProperties.Add(displayName, this);
            return this;
        }
    
        /// <summary>
        ///     Apply a specific css class on an element.
        /// </summary>
        /// <param name="className">The name of the css class that should be placed on the element.</param>
        /// <returns>As <see cref="IColumnBinder{TModel}" /> that is used to construct this column through a fluent API.</returns>
        public IColumnBinder<TModel> WithCss(string className)
        {
            CssClass = className;
            return this;
        }
    
        #endregion
    }
    
  4. And as last the implementation of the IGridConstructor.

    /// <summary>
    ///     Provides an implemention of the <see cref="IGridContructor{TModel}" /> that is used to construct the grid through a
    ///     fluent API.
    /// </summary>
    /// <typeparam name="TModel">The type of the model that the grid will hold.</typeparam>
    public class GridConstructor<TModel> : IGridContructor<TModel>
    {
        #region Constructors
    
        /// <summary>
        ///     Creates a new instance of the <see cref="GridConstructor{TModel}" />.
        /// </summary>
        /// <param name="helper">The <see cref="HtmlHelper" /> that is used to built the model.</param>
        /// <param name="source">The model that is bound to the grid.</param>
        public GridConstructor(HtmlHelper helper, IEnumerable<TModel> source)
        {
            htmlHelper = helper;
            DataSource = source;
    
            builderProperties = new Dictionary<string, IColumnBinder<TModel>>();
        }
    
        #endregion
    
        #region Properties
    
        /// <summary>
        ///     Provides a dictionary that contains all the properties for the builder.
        /// </summary>
        public IDictionary<string, IColumnBinder<TModel>> builderProperties { get; set; }
    
        /// <summary>
        ///     Gets the source that will be bound to the implemented object.
        /// </summary>
        public IEnumerable<TModel> DataSource { get; private set; }
    
        /// <summary>
        ///     Gets the <see cref="HtmlHelper" /> object.
        /// </summary>
        public HtmlHelper htmlHelper { get; private set; }
    
        #endregion
    }
    

Now, how does this works exactely?

  1. The HtmlHelper returns an member that implements an IGridBuilder, so in the example above, it returns a GridBuilder.

  2. On that GridBuilder, there are a couple of elements that you can call, and one more important is the WithColumns method that takes a IColumnBinder action and there's the trick. The implementation of IColumnBinder takes a reference to an IGridConstructor. And it's that constructor that will be fully built up.

So all the things we need to know for rendering, including each css class for a given column are exposed through the GridBuilder.GridContructor

So, a very vert long post, but I hope it helps some people.

Upvotes: 4

Related Questions