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

本文关键字:模型 元数据 获取 IEnumerable | 更新日期: 2023-09-27 18:05:58

我在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. 在函数公共void Bind(Expression> propertySelector)我使用反射来获取模型的元数据。我不太确定这是不是正确的方法。

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

我建议你把签名改成

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

作为

@Html.GridFor(ViewBag.MyCollection)

then in the helper

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

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