views:

1124

answers:

7

I have a grid, and I'm setting the DataSource to a List<IListItem>. What I want is to have the list bind to the underlying type, and disply those properties, rather than the properties defined in IListItem. So:

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

How do I bind to a grid so that if my List<IListItem> contains users I will see the user-specific field? Edit: Note that any given list I want to bind to the Datagrid will be comprised of a single underlying type.

A: 

You'll need to use a Grid template column for this. Inside the template field you'll need to check what the type of the object is and then get the correct property - I recommend creating a method in your code-behind which takes care of this. Thus:

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

In your code-behind:

protected string GetUserSpecificProperty(IListItem obj) {
    if (obj is User) {
        return ((User) obj).UserSpecificField
    } else if (obj is Location) {
        return ((Location obj).LocationSpecificField;
    } else { 
        return "";
    }
}
John Christensen
I'm actually using winforms, but that's fine. My goal is to have my view not depend on the specific types, only on the interface.
Robert
Do TemplateFields exist in winforms? And if you're not going to have the view depend on specific types, why do you want to display the type specific field?An alternative to this might be to create a base class that for User and Location that with a method that outputs the specific field depending on the type of the instance.
John Christensen
I want to present the user with a collection of things, and have them select one. I want to be able to add what kinds of things these will be without changing my view. The first thing I tried was adding string[] GetFieldsForDisplay() to IListItem, but how to bind the string[] to the rows of the grid? And then I don't have the headers...
Robert
What if you didn't bind it to a list of the objects, but instead bound it to an list of arrays. One element of the array (which you just don't bind to a field in the grid) could point back to the original object, which will give you the ability to always get back to the object that was bound to the row.
John Christensen
A: 

I tried projections, and I tried using Convert.ChangeType to get a list of the underlying type, but the DataGrid wouldn't display the fields. I finally settled on creating static methods in each type to return the headers, instance methods to return the display fields (as a list of string) and put them together into a DataTable, and then bind to that. Reasonably clean, and it maintains the separation I wanted between the data types and the display.

Here's the code I use to create the table:

    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;
    }
Robert
Does this mean you don't want any more answers?
ichiban
no, I consider this a workaround, not the actual solution to what I was originally trying to do.
Robert
+1  A: 

As long as you know for sure that the members of the List<IListItem> are all going to be of the same derived type, then here's how to do it, with the "Works on my machine" seal of approval.

First, download BindingListView, which will let you bind generic lists to your DataGridViews.

For this example, I just made a simple form with a DataGridView and randomly either called code to load a list of Users or Locations in 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));
        }
    }
}

The important lines of code are these:

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

and the most important are these:

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

If all items in the list are known to be of the certain derived type, just call ConvertAll to cast them to that type.

Chris Doggett
This is really close to what I'm looking for, but I can't have any reference to the concrete types. I want to be able to add support for different types later without changing anything but my factory that returns IListItems. So reflection calls are OK, casting is not.
Robert
Not sure if it's possible, then, unless you want to use DataTables like in your earlier solution. There's no way to dynamically cast the interface to a concrete class at runtime (that I know of), since the type to cast to has to be known at compile-time. I've even tried using a generic Cast method and Activator.CreateInstance to get the runtime type into a var, but it can't determine the type. Your solution above may be your best bet.
Chris Doggett
@Chris Doggett thanks for looking at it. BindingListView is certainly good to know about.
Robert
I'm not sure off the top of my head how it would work, but if you've looked at LINQ and anonymous types, maybe the IListItem could return a list of strings containing the property names that could be bound, and you could create an anonymous type whose properties are filled via reflection. See http://stackoverflow.com/questions/713521/accessing-c-anonymous-type-objects/713602#713602 (though they say it's evil and you shouldn't do it).
Chris Doggett
I was really hoping for a LINQ solution, but it only works if you cast to a list of the underlying type. I'm not willing to use the nasty hack to get there. As for the IListItem returning the properties to be bound, that's essentially what I've done in my DataTable solution to get the columns.
Robert
If he knew the type at compile (which isn't the case), he could just use .Cast<User>().ToList() - job done. No need for external libraries...
Marc Gravell
A: 

When you use autogeneratecolumns it doesnt automatically do this for you?

Jason Coyne
AutoGenerateColumns doesn't work because the IListItem only contains ID and Name, so those are the only two it'll display, so unfortunately, he either has to cast to a known type or use reflection to fill some some sort of adapter.
Chris Doggett
+3  A: 

Data-binding to lists follows the following strategy:

  1. does the data-source implement IListSource? if so, goto 2 with the result of GetList()
  2. does the data-source implement IList? if not, throw an error; list expected
  3. does the data-source implement ITypedList? if so use this for metadata (exit)
  4. does the data-source have a non-object indexer, public Foo this[int index] (for some Foo)? if so, use typeof(Foo) for metadata
  5. is there anything in the list? if so, use the first item (list[0]) for metadata
  6. no metadata available

List<IListItem> falls into "4" above, since it has a typed indexer of type IListItem - and so it will get the metadata via TypeDescriptor.GetProperties(typeof(IListItem)).

So now, you have three options:

  • write a TypeDescriptionProvider that returns the properties for IListItem - I'm not sure this is feasible since you can't possibly know what the concrete type is given just IListItem
  • use the correctly typed list (List<User> etc) - simply as a simple way of getting an IList with a non-object indexer
  • write an ITypedList wrapper (lots of work)
  • use something like ArrayList (i.e. no public non-object indexer) - very hacky!

My preference is for using the correct type of List<>... here's an AutoCast method that does this for you without having to know the types (with sample usage);

Note that this only works for homogeneous data (i.e. all the objects are the same), and it requires at least one object in the list to infer the type...

// 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
            }
        }
    });
}
Marc Gravell
Tried this with my solution above, removing BindingListView and just setting dataGridView1.DataSource = items.AutoCast(); Works perfectly.
Chris Doggett
Doesn't seem to allow sorting the grid, but that's probably something I'm missing.
Chris Doggett
Sorting on a grid requires IBindingList and a suitable sort implementation. This isn't provided in the main runtime, but there are dozens (more, in fact) of implementations easily available - usually derived from BindingList<T> (and override a few methods) - I've written it at least 3 times myself ;-p Let me know if you have problems tracking one down, and I'll provide one for you...
Marc Gravell
A: 

My suggestion would be to dynamically create the columns in the grid for the extra properties and create either a function in IListItem that gives a list of available columns - or use object inspection to identify the columns available for the type.

The GUI would then be much more generic, and you would not have as much UI control over the extra columns - but they would be dynamic.

Non-checked/compiled 'psuedo code';

public interface IListItem
{
    IList<string> ExtraProperties;


    ... your old code.
}

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

and in form loading

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

If you are willing to use a ListView based solution, the data-bindable version ObjectListView will let you do this. It reads the exposed properties of the DataSource and creates columns to show each property. You can combine it with BindingListView.

It also looks nicer than a grid :)

Grammarian