如何从IEnumerable中获取模型元数据?

时间:2014-05-31 15:30:36

标签: c# asp.net-mvc asp.net-mvc-4

我在MVC中有GridView,它按以下方式构建:

@model IEnumerable

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

你在上面看到的是我构建了我的模型的网格,这是一个IEnumerable,我将3列绑定到它。

我的HtmlHelper的代码如下:

/// <summary>
///     Provides a way to extend the <see cref="HtmlHelper" />.
/// </summary>
public static class HtmlHelperExtensions
{
    #region Grid

    /// <summary>
    ///     Constructs a grid for a given model by using a fluent API.
    /// </summary>
    /// <typeparam name="TModel">The type of the model that is bound to the grid.</typeparam>
    /// <param name="htmlHelper">The <see cref="HtmlHelper" /> which is used to create the grid.</param>
    /// <returns>An <see cref="IGridBuilder{TModel}" /> that is used to construct the grid.</returns>
    public static IGridBuilder<TModel> GridFor<TModel>(this HtmlHelper<IEnumerable<TModel>> htmlHelper)
    {
        return new GridBuilder<TModel>(htmlHelper);
    }

    #endregion
}

我将在此方法中返回一个接口,以允许通过流畅的API进行构造。 该接口的代码如下:

/// <summary>
///     When implemented on a class, this class acts as an <see cref="IHtmlString" /> that can construct a grid by using a
///     fluent API.
/// </summary>
/// <typeparam name="TModel">The type of the model that is bound to the grid.</typeparam>
public interface IGridBuilder<TModel> : IHtmlString
{
    #region Properties

    /// <summary>
    ///     Gets the name of the <see cref="IGridBuilder{TModel}" />.
    ///     The outer div of the grid will have an id that matches this name.
    /// </summary>
    string Name { get; }

    /// <summary>
    ///     The <see cref="HtmlHelper" /> that is used to build the grid.
    /// </summary>
    HtmlHelper HtmlHelper { get; }

    #endregion

    #region Methods

    /// <summary>
    ///     Sets the name of the <see cref="IGridBuilder{TModel}" />.
    /// </summary>
    /// <param name="name">The name that the <see cref="IGridBuilder{TModel}" /> should have.</param>
    /// <returns>An <see cref="IGridBuilder{TModel}" /> that is used to construct the grid.</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<IGridBuilder<TModel>> bindAllColumns);

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

    #endregion
}

然后,当然,我已经实现了它:

/// <summary>
///     An implementation of the <see cref="IGridBuilder{TModel}" /> that is used to build a grid.
/// </summary>
/// <typeparam name="TModel">The type of the model that is bound to the grid.</typeparam>
public class GridBuilder<TModel> : IGridBuilder<TModel> 
{
    #region Constructors

    /// <summary>
    ///     Creates a new instance of the <see cref="GridBuilder{TModel}" />.
    /// </summary>
    /// <param name="htmlHelper">The <see cref="HtmlHelper{TModel}" /> that is used to render this one.</param>
    public GridBuilder(HtmlHelper<IEnumerable<TModel>> htmlHelper)
    {
        HtmlHelper = htmlHelper;
        properties = new Dictionary<string, List<string>>();
    }

    #endregion

    #region Properties

    /// <summary>
    ///     A <see cref="List{TKey}" /> that conains the names of the property and the display name that belongs
    ///     to the property.
    /// </summary>

    public readonly Dictionary<string, List<string>> properties;

    #endregion

    #region IGridBuilder Members

    /// <summary>
    ///     Gets the name of the <see cref="IGridBuilder{TModel}" />.
    ///     The outer div of the grid will have an id that matches this name.
    /// </summary>
    public string Name { get; private set; }

    /// <summary>
    ///     The <see cref="HtmlHelper" /> that is used to build the grid.
    /// </summary>
    public HtmlHelper HtmlHelper { get; private set; }

    /// <summary>
    ///     Sets the name of the <see cref="IGridBuilder{TModel}" />.
    /// </summary>
    /// <param name="name">The name that the <see cref="IGridBuilder{TModel}" /> should have.</param>
    /// <returns>An <see cref="IGridBuilder{TModel}" /> that is used to construct the grid.</returns>
    public IGridBuilder<TModel> WithName(string name)
    {
        Name = name;
        return this;
    }

    /// <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>
    public void Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector)
    {
        string name = ExpressionHelper.GetExpressionText(propertySelector);
        name = HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        ModelMetadata metadata =
            ModelMetadataProviders.Current.GetMetadataForProperty(() => Activator.CreateInstance<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;

        var items = (from TModel entity in HtmlHelper.ViewData.Model as IEnumerable select propertySelector.Compile().Invoke(entity).ToString()).ToList();

        properties.Add(displayName, items);
    }

    /// <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<IGridBuilder<TModel>> bindAllColumns)
    {
        bindAllColumns(this);
        return this;
    }

    #endregion

    #region IHtmlString Members

    /// <summary>
    ///     Returns an HTML-encoded string.
    /// </summary>
    /// <returns>Returns an HTML-encoded string.</returns>
    public string ToHtmlString()
    {
        var output = new StringBuilder();

        BaseElementBuilder parentBuilder = DivFactory.DivElement().WithCssClass("gridHolder v-scroll").WithId(Name);
        BaseElementBuilder headerBuilder = DivFactory.DivElement().WithCssClass("header");

        output.Append(parentBuilder.ToString(TagRenderMode.StartTag));
        output.Append(headerBuilder.ToString(TagRenderMode.StartTag));

        var index = 0;
        foreach (
           var propertyBuilder in
                properties.Select(
                    property =>
                        (index == 0)
                            ? DivFactory.DivElement().WithCssClass("inline").WithInnerHtml(property.Key)
                            : DivFactory.DivElement()
                                .WithCssClass("inline fixed right")
                                .WithInnerHtml(property.Key)))
        {
            output.Append(propertyBuilder);
            index += 1;
        }

        output.Append(headerBuilder.ToString(TagRenderMode.EndTag));

        for (int i = 0; i < properties.First().Value.Count(); i++)
        {
            BaseElementBuilder rowBuilder = DivFactory.DivElement().WithCssClass("row");
            output.Append(rowBuilder.ToString(TagRenderMode.StartTag));

            BaseElementBuilder iconBuilder = DivFactory.DivElement().WithCssClass("inline icon").WithInnerHtml("<img src=\"~/Resources/Icons/Grid/Pages/Page.png\" />");
            output.Append(iconBuilder.ToString(TagRenderMode.StartTag));
            output.Append(iconBuilder.ToString(TagRenderMode.EndTag));

            int loopIndex = 0;
            foreach (var propertyBuilder in properties)
            {
                var value = propertyBuilder.Value[i];
                BaseElementBuilder propertyConstructor = (loopIndex == 0)
                    ? DivFactory.DivElement().WithCssClass("inline").WithInnerHtml(value)
                    : DivFactory.DivElement().WithCssClass("inline fixed right").WithInnerHtml(value);

                loopIndex += 1;

                output.Append(propertyConstructor.ToString(TagRenderMode.Normal));
            }

            output.Append(rowBuilder.ToString(TagRenderMode.EndTag));
        }

        output.Append(parentBuilder.ToString(TagRenderMode.EndTag));

        return output.ToString();
    }

    #endregion
}

在这个实现中,我关注的是一件小事:

  1. 在函数 public void Bind(Expression&gt; propertySelector)中,我使用反射来获取模型的元数据。不太确定这是否正确。

1 个答案:

答案 0 :(得分:3)

我建议您将签名更改为

public static IGridBuilder<TModel> GridFor<TModel>(this HtmlHelper<TModel> htmlHelper, IEnumerable collection)

并将其用作

@Html.GridFor(ViewBag.MyCollection)

然后在帮手

// Get the type in the collection
Type type = GetCollectionType(collection); // See below
// Get the metadata of the type in the collection
ModelMetadata typeMetadata = ModelMetadataProviders.Current
  .GetMetadataForType(null, type);

然后循环集合

foreach (var item in collection)
{
  ModelMetadata itemMetadata = ModelMetadataProviders
    .Current.GetMetadataForType(() => item, type);
  // Use itemMetadata.Properties to generate the columns 

获取类型可能很复杂,因为IGrouping,IDictionary和Lookup也实现了IEnumerable(我没有包含用于检查这些的代码)

private static Type GetCollectionType(IEnumerable collection)
{
  Type type = collection.GetType();
  if (type.IsGenericType)
  {
    return type.GetInterfaces().Where(t => t.IsGenericType)
      .Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
      .Single().GetGenericArguments().Last();
  }
  else if (collection.GetType().IsArray)
  {
    return type.GetElementType();
  }
  else
  {
        // Who knows?
        return null;
   }
}

编辑1 :使用现有代码的替代方法是更改​​Bind方法,如下所示

public void Bind<TItem>(Expression<Func<TModel, TItem>> propertySelector)
{
  string name = ExpressionHelper.GetExpressionText(propertySelector);
  name = HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
  IEnumerable collection = HtmlHelper.ViewData.Model as IEnumerable;
  foreach (var item in collection)
  {
    ModelMetadata modelMetadata = ModelMetadataProviders.Current
      .GetMetadataForType(() => item, item.GetType())
      .Properties.First(m => m.PropertyName == name);
    string displayName = modelMetadata.GetDisplayName();
    if (!properties.ContainsKey(displayName))
    {
      properties[displayName] = new List<string>();
    }
    // Take into account display format and null values
    string format = modelMetadata.DisplayFormatString ?? "{0}";
    properties[displayName].Add(string.Format(format, 
      modelMetadata.Model ??  modelMetadata.NullDisplayText));     
  }
}