【问题标题】:Fluent API: Reference types take values from eachotherFluent API:引用类型从彼此获取值
【发布时间】:2014-06-03 08:46:06
【问题描述】:

我目前正在为 MVC 编写一个 HtmlHelper 扩展的网格,但我遇到了一个奇怪的问题。

首先,这是我用来构建网格的代码:

RenderedOutput = HtmlHelper.GridFor(Model)
.WithColumns(column =>
{
    column.Bind(x => x.Name)
        .WithCss("inline");
    column.Bind(x => x.Age)
        .WithCss("inline fixed right");
})
.Render();

现在,整个 Fluent API 都是使用接口构建的,为了让 copy-past 的工作更小,我不在这里发布接口,但我会发布实现:

首先,HtmlHelper:

public static IGridBuilder<TEntity> GridFor<TModel, TEntity>(this HtmlHelper<TModel> htmlHelper,
    IEnumerable<TEntity> dataSource)
{
    return new GridBuilder<TEntity>(htmlHelper, dataSource);
}

所以这会返回一个 GridBuilder,它的实现在这里:

#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

}

GridBuilder 采用 IColumnBuilder 的 Action 来构建列,所以这里是:

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
}

首先,这是我的第一个流畅的接口实现,所以如果这不是好的方法,请指出正确的方向。

情况:

  • 当我构建网格时,我传入一个绑定列的动作,这些列也再次流畅,(例如,我可以在列上添加一个类)。现在,为了渲染这些,我认为在我的 IGridBuilder 中保存对所有 IColumnBuilder 实例的引用是一个好主意,这样在我的 GridBuilder 渲染方法中我可以执行类似的操作

    // 在列之前渲染。

    foreach(myColumns 中的 var c) { c.渲染(); }

    // 在列之后渲染。

因此,我在 GridBuilder 中创建了一个列表,其中将包含所有 ColumnBuilder。 当我执行 WithColumns 时,传递了我的 GridBuilder 对象 (this),然后在 ColumnBuilder 中,在每个 Bind() 函数上,我将 ColumnBuilder 对象添加到传递的引用中。

但这有一个奇怪的行为(例如,包含 ColumnBuilders 的列表,确实都与最后执行的 ColumnBuilder 的属性相匹配)。

【问题讨论】:

  • cmets 有必要吗?他们增加了很多噪音。您能否清楚地说明您在哪里遇到了问题——也许给出一个带有预期/实际结果的用例?
  • @DaveParsons 猜测问题是.WithColumns 只创建一个列构建器,然后绑定多次,覆盖以前的绑定?我认为.WithColumns 应该提供一个IColumnBuilderFactory,它有一个方法AddNew。无论是那个还是Bind 都应该是一个奇怪的列扩展方法,它允许列为空并创建一个新的
  • 所以,我已经删除了代码中的 cmets 并进行了更详细的解释。希望有帮助。 @JamesBarrass您介意为您提到的工厂提供一个小例子吗?再次检查我自己的代码后,您所讲述的故事似乎是正确的,但我没有直接看到我如何特别解决这个问题。因此,如果您能提供一个小例子,那就太好了。提前致谢。

标签: c# asp.net-mvc html-helper fluent-interface


【解决方案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
}

而 GridBuilder WithColumns 变为

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;
}

这个有用法

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();

方法二

通过欺骗和忽略这一点(使用 FluentAPI 替换它),替代方案的作用大致相同,基本上它与您当前的用法相同,但 Bind 变为

public IColumnBuilder<TModel> Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector)
{
    var builder = new ColumnBuilder<TModel>(GridBuilderReference);

    GridBuilderReference.ColumnBuilders.Add(builder);

    return builder;
}

它依赖于 GC 整理第一个(无用的)实例

所以.....

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();

显示方法 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>();
        }
    }
}

【讨论】:

  • 我查看了您的代码,但您使用 fluent API 的方法存在一些问题。首先,我认为对于列构建器,您需要添加“builder”而不是“this”。其次,它现在不能正常工作了,因为在它上面添加一个 css 类确实依赖于此,而不是依赖于创建的构建器实例。你也可以就那个提供建议吗?我认为第一种方法是可行的,但我更喜欢我现在使用的语法,由于工厂位于两者之间,因此在 ColumnBinder 上不使用“New()”。
  • 另外,当我使用你的工厂时,你的 foreach 循环不起作用,因为工厂不是 IEnumerable。所以还有另一个错误。
  • 好的,我已经稍微修改了代码,现在它可以工作了。总体思路很好。你能给我提供调整后的代码,让我在没有工厂的情况下工作吗?因为我更喜欢这种语法。提前致谢。
  • @Complexity,你在第二种方法中是对的,它应该是builder。请注意,在任何不是Bind 的后续调用中,builder 将变为 this,因此方法 bind 实际上是在分隔您的列。抱歉,我在没有 IDE 的情况下将其从头顶上写下来,所以我怀疑其中可能存在一些错误
  • 有几个,但它正在进行一些修改。第二种方法中的 this 引用是什么意思,因为现在还不是很清楚。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-10
  • 1970-01-01
相关资源
最近更新 更多