【问题标题】:C# grid DataSource polymorphismC# 网格数据源多态性
【发布时间】:2009-05-29 18:13:32
【问题描述】:

我有一个网格,我将DataSource 设置为List<IListItem>。我想要的是让列表绑定到基础类型,并显示这些属性,而不是IListItem 中定义的属性。所以:

public interface IListItem
{
    string Id;
    string Name;
}

public class User : IListItem
{
    string Id { get; set; };
    string Name { get; set; };
    string UserSpecificField { get; set; };
}

public class Location : IListItem
{
    string Id { get; set; };
    string Name { get; set; };
    string LocationSpecificField { get; set; };
}

如何绑定到网格,以便如果我的List<IListItem> 包含用户,我将看到用户特定的字段?编辑:请注意,我想绑定到 Datagrid 的任何给定列表都将由单个基础类型组成。

【问题讨论】:

  • 您是否考虑过转换为数据表并返回?

标签: c# winforms datagridview


【解决方案1】:

数据绑定到列表遵循以下策略:

  1. 数据源是否实现IListSource?如果是这样,转到 2,结果为 GetList()
  2. 数据源是否实现IList?如果不是,则抛出错误;预期列表
  3. 数据源是否实现ITypedList?如果是这样,请将此用于元数据(退出)
  4. 数据源是否有非对象索引器public Foo this[int index](对于某些Foo)?如果是这样,请使用 typeof(Foo) 获取元数据
  5. 列表中有任何内容吗?如果是这样,请使用第一项 (list[0]) 作为元数据
  6. 没有可用的元数据

List<IListItem> 属于上面的“4”,因为它有一个类型为 IListItem 的类型化索引器 - 因此它将通过 TypeDescriptor.GetProperties(typeof(IListItem)) 获取元数据。

所以现在,您有三个选择:

  • 写一个TypeDescriptionProvider 来返回IListItem 的属性——我不确定这是否可行,因为你不可能知道IListItem 给出的具体类型是什么
  • 使用正确键入的列表(List<User> 等) - 只是作为使用非对象索引器获取 IList 的简单方法
  • 编写一个ITypedList 包装器(大量工作)
  • 使用 ArrayList 之类的东西(即没有公共的非对象索引器) - 非常老套!

我的偏好是使用正确类型的 List<>...这是一个 AutoCast 方法,它可以为您做到这一点无需知道类型(使用示例);

请注意,这仅适用于同质数据(即所有对象都相同),并且它需要列表中至少有一个对象来推断类型...

// infers the correct list type from the contents
static IList AutoCast(this IList list) {
    if (list == null) throw new ArgumentNullException("list");
    if (list.Count == 0) throw new InvalidOperationException(
          "Cannot AutoCast an empty list");
    Type type = list[0].GetType();
    IList result = (IList) Activator.CreateInstance(typeof(List<>)
          .MakeGenericType(type), list.Count);
    foreach (object obj in list) result.Add(obj);
    return result;
}
// usage
[STAThread]
static void Main() {
    Application.EnableVisualStyles();
    List<IListItem> data = new List<IListItem> {
        new User { Id = "1", Name = "abc", UserSpecificField = "def"},
        new User { Id = "2", Name = "ghi", UserSpecificField = "jkl"},
    };
    ShowData(data, "Before change - no UserSpecifiedField");
    ShowData(data.AutoCast(), "After change - has UserSpecifiedField");
}
static void ShowData(object dataSource, string caption) {
    Application.Run(new Form {
        Text = caption,
        Controls = {
            new DataGridView {
                Dock = DockStyle.Fill,
                DataSource = dataSource,
                AllowUserToAddRows = false,
                AllowUserToDeleteRows = false
            }
        }
    });
}

【讨论】:

  • 用我上面的解决方案试过这个,删除 BindingListView 并设置 dataGridView1.DataSource = items.AutoCast();完美运行。
  • 似乎不允许对网格进行排序,但这可能是我缺少的东西。
  • 在网格上排序需要 IBindingList 和合适的排序实现。这不是在主运行时提供的,但是有几十个(实际上更多)很容易获得的实现——通常从 BindingList 派生(并覆盖一些方法)——我自己至少写了 3 次;-p 如果您在追踪一个时遇到问题,请告诉我,我会为您提供一个...
【解决方案2】:

只要您确定 List 的成员都将属于相同的派生类型,那么下面就是如何做到这一点,并带有“在我的机器上工作”的批准印章。

首先,下载BindingListView,它可以让您将通用列表绑定到您的 DataGridViews。

对于这个例子,我只是用 DataGridView 制作了一个简单的表单,并随机调用代码来加载 Form1_Load() 中的用户或位置列表。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using Equin.ApplicationFramework;

namespace DGVTest
{
    public interface IListItem
    {
        string Id { get; }
        string Name { get; }
    }

    public class User : IListItem
    {
        public string UserSpecificField { get; set; }
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public class Location : IListItem
    {
        public string LocationSpecificField { get; set; }
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void InitColumns(bool useUsers)
        {
            if (dataGridView1.ColumnCount > 0)
            {
                return;
            }

            DataGridViewCellStyle gridViewCellStyle = new DataGridViewCellStyle();

            DataGridViewTextBoxColumn IDColumn = new DataGridViewTextBoxColumn();
            DataGridViewTextBoxColumn NameColumn = new DataGridViewTextBoxColumn();
            DataGridViewTextBoxColumn DerivedSpecificColumn = new DataGridViewTextBoxColumn();

            IDColumn.DataPropertyName = "ID";
            IDColumn.HeaderText = "ID";
            IDColumn.Name = "IDColumn";

            NameColumn.DataPropertyName = "Name";
            NameColumn.HeaderText = "Name";
            NameColumn.Name = "NameColumn";

            DerivedSpecificColumn.DataPropertyName = useUsers ? "UserSpecificField" : "LocationSpecificField";
            DerivedSpecificColumn.HeaderText = "Derived Specific";
            DerivedSpecificColumn.Name = "DerivedSpecificColumn";

            dataGridView1.Columns.AddRange(
                new DataGridViewColumn[]
                    {
                        IDColumn,
                        NameColumn,
                        DerivedSpecificColumn
                    });

            gridViewCellStyle.SelectionBackColor = Color.LightGray;
            gridViewCellStyle.SelectionForeColor = Color.Black;
            dataGridView1.RowsDefaultCellStyle = gridViewCellStyle;
        }

        public static void BindGenericList<T>(DataGridView gridView, List<T> list)
        {
            gridView.DataSource = new BindingListView<T>(list);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            dataGridView1.AutoGenerateColumns = false;

            Random rand = new Random();

            bool useUsers = rand.Next(0, 2) == 0;

            InitColumns(useUsers);

            if(useUsers)
            {
                TestUsers();
            }
            else
            {
                TestLocations();
            }

        }

        private void TestUsers()
        {
            List<IListItem> items =
                new List<IListItem>
                    {
                        new User {Id = "1", Name = "User1", UserSpecificField = "Test User 1"},
                        new User {Id = "2", Name = "User2", UserSpecificField = "Test User 2"},
                        new User {Id = "3", Name = "User3", UserSpecificField = "Test User 3"},
                        new User {Id = "4", Name = "User4", UserSpecificField = "Test User 4"}
                    };


            BindGenericList(dataGridView1, items.ConvertAll(item => (User)item));
        }

        private void TestLocations()
        {
            List<IListItem> items =
                new List<IListItem>
                    {
                        new Location {Id = "1", Name = "Location1", LocationSpecificField = "Test Location 1"},
                        new Location {Id = "2", Name = "Location2", LocationSpecificField = "Test Location 2"},
                        new Location {Id = "3", Name = "Location3", LocationSpecificField = "Test Location 3"},
                        new Location {Id = "4", Name = "Location4", LocationSpecificField = "Test Location 4"}
                    };


            BindGenericList(dataGridView1, items.ConvertAll(item => (Location)item));
        }
    }
}

重要的代码行如下:

DerivedSpecificColumn.DataPropertyName = useUsers ? "UserSpecificField" : "LocationSpecificField"; // obviously need to bind to the derived field

public static void BindGenericList<T>(DataGridView gridView, List<T> list)
{
    gridView.DataSource = new BindingListView<T>(list);
}

dataGridView1.AutoGenerateColumns = false; // Be specific about which columns to show

最重要的是这些:

BindGenericList(dataGridView1, items.ConvertAll(item => (User)item));
BindGenericList(dataGridView1, items.ConvertAll(item => (Location)item));

如果已知列表中的所有项目都属于某个派生类型,只需调用 ConvertAll 将它们强制转换为该类型。

【讨论】:

  • 这真的很接近我正在寻找的东西,但我无法参考具体类型。我希望以后能够添加对不同类型的支持,而无需更改任何内容,但我的工厂返回 IListItems。所以反射调用是可以的,转换不是。
  • 不确定是否可行,除非您想像之前的解决方案一样使用 DataTables。无法在运行时将接口动态转换为具体类(我知道),因为必须在编译时知道要转换的类型。我什至尝试使用通用 Cast 方法和 Activator.CreateInstance 将运行时类型转换为 var,但它无法确定类型。您上面的解决方案可能是您最好的选择。
  • @Chris Doggett 感谢您的关注。 BindingListView 确实很值得了解。
  • 我不确定它是如何工作的,但如果您查看过 LINQ 和匿名类型,也许 IListItem 可以返回包含属性名称的字符串列表可以绑定,您可以创建一个匿名类型,其属性通过反射填充。见stackoverflow.com/questions/713521/…(尽管他们说这是邪恶的,你不应该这样做)。
  • 我真的很希望有一个 LINQ 解决方案,但它只有在转换为基础类型列表时才有效。我不愿意使用讨厌的黑客来到达那里。至于返回要绑定的属性的 IListItem,这基本上就是我在 DataTable 解决方案中为获取列所做的工作。
【解决方案3】:

您需要为此使用 Grid 模板列。在模板字段中,您需要检查对象的类型,然后获取正确的属性 - 我建议在您的代码隐藏中创建一个方法来处理这个问题。因此:

<asp:TemplateField HeaderText="PolymorphicField">
    <ItemTemplate>
        <%#GetUserSpecificProperty(Container.DataItem)%>
    </ItemTemplate>
</asp:TemplateField>

在您的代码隐藏中:

protected string GetUserSpecificProperty(IListItem obj) {
    if (obj is User) {
        return ((User) obj).UserSpecificField
    } else if (obj is Location) {
        return ((Location obj).LocationSpecificField;
    } else { 
        return "";
    }
}

【讨论】:

  • 我实际上正在使用winforms,但这很好。我的目标是让我的视图不依赖于特定类型,而只依赖于界面。
  • Winforms中是否存在TemplateFields?如果您不打算让视图依赖于特定类型,为什么要显示特定于类型的字段?对此的替代方法可能是为 User 和 Location 创建一个基类,该基类具有根据实例类型输出特定字段的方法。
  • 我想向用户展示一组东西,并让他们选择一个。我希望能够在不改变我的观点的情况下添加这些内容。我尝试的第一件事是将 string[] GetFieldsForDisplay() 添加到 IListItem,但是如何将 string[] 绑定到网格的行?然后我没有标题...
  • 如果您没有将其绑定到对象列表,而是将其绑定到数组列表,该怎么办。数组的一个元素(您只是不绑定到网格中的字段)可以指向原始对象,这将使您能够始终返回到绑定到行的对象。跨度>
【解决方案4】:

我尝试了投影,并尝试使用 Convert.ChangeType 来获取基础类型的列表,但 DataGrid 不会显示字段。我最终决定在每种类型中创建静态方法以返回标头,实例方法以返回显示字段(作为字符串列表)并将它们一起放入 DataTable,然后绑定到它。相当干净,它保持了我想要的数据类型和显示之间的分离。

这是我用来创建表格的代码:

    DataTable GetConflictTable()
    {
        Type type = _conflictEnumerator.Current[0].GetType();
        List<string> headers = null;
        foreach (var mi in type.GetMethods(BindingFlags.Static | BindingFlags.Public))
        {
            if (mi.Name == "GetHeaders")
            {
                headers = mi.Invoke(null, null) as List<string>;
                break;
            }
        }
        var table = new DataTable();
        if (headers != null)
        {
            foreach (var h in headers)
            {
                table.Columns.Add(h);
            }
            foreach (var c in _conflictEnumerator.Current)
            {
                table.Rows.Add(c.GetFieldsForDisplay());
            }
        }
        return table;
    }

【讨论】:

  • 这是否意味着您不再需要任何答案?
  • 不,我认为这是一种解决方法,而不是我最初尝试做的实际解决方案。
【解决方案5】:

当您使用 autogeneratecolumns 时,它不会自动为您执行此操作?

【讨论】:

  • AutoGenerateColumns 不起作用,因为 IListItem 只包含 ID 和 Name,所以它们是唯一会显示的两个,所以不幸的是,他要么必须转换为已知类型,要么使用反射来填充某种适配器。
【解决方案6】:

我的建议是在网格中为额外属性动态创建列,并在 IListItem 中创建一个提供可用列列表的函数 - 或使用对象检查来识别该类型可用的列。

GUI 会更加通用,您不会对额外的列进行太多的 UI 控制 - 但它们会是动态的。

未经检查/编译的“伪代码”;

public interface IListItem
{
    IList<string> ExtraProperties;


    ... your old code.
}

public class User : IListItem
{
   .. your old code
    public IList<string> ExtraProperties { return new List { "UserSpecificField" } }
}

在表单加载中

foreach(string columnName in firstListItem.ExtraProperties)
{
     dataGridView.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = columnName, HeaderText = columnName );
}

【讨论】:

    【解决方案7】:

    如果您愿意使用基于 ListView 的解决方案,数据绑定版本ObjectListView 将允许您这样做。它读取 DataSource 的公开属性并创建列来显示每个属性。您可以将其与 BindingListView 结合使用。

    它看起来也比网格更好:)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-03-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-17
      相关资源
      最近更新 更多