C# grid DataSource polymorphism - c#

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.

Data-binding to lists follows the following strategy:
does the data-source implement IListSource? if so, goto 2 with the result of GetList()
does the data-source implement IList? if not, throw an error; list expected
does the data-source implement ITypedList? if so use this for metadata (exit)
does the data-source have a non-object indexer, public Foo this[int index] (for some Foo)? if so, use typeof(Foo) for metadata
is there anything in the list? if so, use the first item (list[0]) for metadata
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
}
}
});
}

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.

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

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

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

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

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 :)

Related

DataGridView Databinding to List<List<T>>

Given the code
class Foo {
public string Value {get; set;}
public int Id {get; set;}
}
List<List<Foo>> fooList = new List<List<Foo>>();
Is there a way to bind a Multidim ICollection to a DataGridView on the property Value, where when you change a cell, the Value property of the object updates?
In this case, each instance of Foo in the list will represent one cell in the DataGridView and the rows/ columns are being preserved as they would be in the multidim ICollection
By Multidim I mean something to the affect of:
List<List<Foo>] => [
List<Foo> => [0,1,2,3,4,5]
List<Foo> => [0,1,2,3,4,5]
List<Foo> => [0,1,2,3,4,5]
List<Foo> => [0,1,2,3,4,5]
]
Where each element in the nested list is actually and instance of Foo.
Implementing IListSource and mapping to DataTabe internally
You can create a custom data source which implements IListSource and set it as data source of DataGridView. To implement the interface properly to satisfy your requirement:
In constructor, accept original list and map it to a DataTable.
Subscribe to ListChanged event of the DefaultView property of you data table and apply changes to your original list.
For GetList method, return the mapped data table.
Then when you bind DataGridView to your new data source, all the editing operations will immediately reflect in your original list:
dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);
ListListDataSource Implementation
public class ListListDataSource<T> : IListSource
{
List<List<T>> data;
DataTable table;
public ListListDataSource(List<List<T>> list)
{
this.data = list;
table = new DataTable();
for (int i = 0; i < list.First().Count(); i++)
{
TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>()
.Where(p => p.IsBrowsable).ToList().ForEach(p =>
{
if (p.IsBrowsable)
{
var c = new DataColumn($"[{i}].{p.Name}", p.PropertyType);
c.ReadOnly = p.IsReadOnly;
table.Columns.Add(c);
}
});
}
foreach (var innerList in list)
{
table.Rows.Add(innerList.SelectMany(
x => TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>()
.Where(p => p.IsBrowsable).Select(p => p.GetValue(x))).ToArray());
}
table.DefaultView.AllowDelete = false;
table.DefaultView.AllowNew = false;
table.DefaultView.ListChanged += DefaultView_ListChanged;
}
public bool ContainsListCollection => false;
public IList GetList()
{
return table.DefaultView;
}
private void DefaultView_ListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType != ListChangedType.ItemChanged)
throw new NotSupportedException();
var match = Regex.Match(e.PropertyDescriptor.Name, #"\[(\d+)\]\.(\w+)");
var index = int.Parse(match.Groups[1].Value);
var propertyName = match.Groups[2].Value;
typeof(T).GetProperty(propertyName).SetValue(data[e.NewIndex][index],
table.Rows[e.NewIndex][e.PropertyDescriptor.Name]);
}
}
Then bind your list to DataGridView like this:
List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
foos = new List<List<Foo>>{
new List<Foo>(){
new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
},
new List<Foo>() {
new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
},
};
dataGridView1.DataSource = new ListListDataSource<Foo>(foos);
}
And when you edit data in DataGridView, in fact you are editing the original list.
Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:
public class Foo
{
[Browsable(false)]
public int Id { get; set; }
public string Value { get; set; }
}
The problem you describe can be solved a couple of different ways. One is to “flatten” each List<Foo>. Basically this will flatten ALL the Foo items in a list into a “single” string. With this approach as I commented, you would end up with one column and each row would be a “flattened” List<Foo>. Each cell may have a different number of Foo items in the string.
In this case and as others, this may not be the desired result. Since you have a List of Lists, then a “Master-Detail" approach using two (2) grids may make things easier. In this approach, the first grid (master) would have one (1) column and each row would be a List<Foo>. Since we already know the grid will not display this LIST into a single cell AND we don’t want to “flatten” the list, then this is where the second (detail) grid comes into play. The first grid displays all the lists of Foo, and whichever “row” is selected, the second grid (detail) will display all the List<Foo> items.
An example may work best to show what I mean. First, we need to make an additional class. Reason being that is if we use a List<List<Foo>> as a DataSource to the master grid, it will show something like…
As shown the two columns are going to be the List “Capacity” and “Count.” This may work; however, it may be confusing to the user. That is why we want this other class. It is a simple “wrapper” around the List<Foo> and to display this we will add a “Name” property to this class. This will be displayed in the master grid.
Given the current modified Foo class…
public class Foo {
public string Value { get; set; }
public int Id { get; set; }
}
This FooList class may look something like…
public class FooList {
public string ListName { get; set; }
public List<Foo> TheFooList { get; set; }
}
A List<FooList> would display something like…
Now, when the user “selects” a row in the first “Master” grid, the second “Detail" grid will display all the Foo items in that list. A full example is below. Drop two grids onto a form and copy the code below to follow.
To help, a method that returns a List<Foo> where there are a random number of Foo items in each list. This method may look something like below with the global rand Random variable to get a random number of Foo items to add to the list in addition to setting a random Value for each Foo object.
Random rand = new Random();
private List<Foo> GettRandomNumberOfFooList() {
int numberOfFoo = rand.Next(2, 20);
List<Foo> fooList = new List<Foo>();
for (int i = 0; i < numberOfFoo; i++) {
fooList.Add(new Foo { Id = i, Value = rand.Next(1, 100).ToString() });
}
return fooList;
}
We can use this method to create a List<FooList> for testing. The master grids DataSource will be this list. Then, to determine which list to display in the details grid, we will simply use the selected FooList.TheFooList property.
Next, we need a trigger to know when to “change” the details data source. In this case I used the grids, RowEnter method to change the details grids data source.
Below is the code described above. The master grid will have 15 FooList items.
List<FooList> FooLists;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
FooLists = new List<FooList>();
for (int i = 0; i < 15; i++) {
FooLists.Add(new FooList { ListName = "Foo List " + (i + 1), TheFooList = GettRandomNumberOfFooList() });
}
dataGridView1.DataSource = FooLists;
dataGridView2.DataSource = FooLists[0].TheFooList;
}
private void dataGridView1_RowEnter(object sender, DataGridViewCellEventArgs e) {
dataGridView2.DataSource = FooLists[e.RowIndex].TheFooList;
}
This should produce something like...
Lastly, this is just an example and using a BindingList/BindingSource may make things easier. This is a very simple example of using a “Master-Detail” approach with a List of Lists.
Using Custom TypeDescriptor
An interesting approach is creating a new data source using a custom TypeDescriptor.
Type descriptor provide information about type, including list of properties and getting and setting property values. DataTable also works the same way, to show list of columns in DataGridView, it returns a list of property descriptors containing properties per column.
Then when you bind DataGridView to your new data source, you are in fact editing the original list:
dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);
ListListDataSource implementation using TypeDescriptor
Here I've created a custom type descriptor for each inner list to treat is as a single object having a few properties. The properties are all properties of each element of the inner list and I've created a property descriptor for properties:
public class ListListDataSource<T> : List<FlatList>
{
public ListListDataSource(List<List<T>> list)
{
this.AddRange(list.Select(x =>
new FlatList(x.Cast<object>().ToList(), typeof(T))));
}
}
public class FlatList : CustomTypeDescriptor
{
private List<object> data;
private Type type;
public FlatList(List<object> data, Type type)
{
this.data = data;
this.type = type;
}
public override PropertyDescriptorCollection GetProperties()
{
return this.GetProperties(new Attribute[] { });
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
var properties = new List<PropertyDescriptor>();
for (int i = 0; i < data.Count; i++)
{
foreach (PropertyDescriptor p in TypeDescriptor.GetProperties(type))
properties.Add(new FlatListProperty(i, p));
}
return new PropertyDescriptorCollection(properties.ToArray());
}
public object this[int i]
{
get => data[i];
set => data[i] = value;
}
}
public class FlatListProperty : PropertyDescriptor
{
int index;
PropertyDescriptor originalProperty;
public FlatListProperty(int index, PropertyDescriptor originalProperty)
: base($"[{index}].{originalProperty.Name}",
originalProperty.Attributes.Cast<Attribute>().ToArray())
{
this.index = index;
this.originalProperty = originalProperty;
}
public override Type ComponentType => typeof(FlatList);
public override bool IsReadOnly => false;
public override Type PropertyType => originalProperty.PropertyType;
public override bool CanResetValue(object component) => false;
public override object GetValue(object component) =>
originalProperty.GetValue(((FlatList)component)[index]);
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) =>
originalProperty.SetValue(((FlatList)component)[index], value);
public override bool ShouldSerializeValue(object component) => true;
}
To bind data:
List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
foos = new List<List<Foo>>{
new List<Foo>(){
new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
},
new List<Foo>() {
new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
},
};
dataGridView1.DataSource = new ListListDataSource<Foo>(foos);
}
And when you edit data in DataGridView, in fact you are editing the original list.
Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:
public class Foo
{
[Browsable(false)]
public int Id { get; set; }
public string Value { get; set; }
}
Flattening the List<List<T>> into a List<T>
If showing data in a flattened structure for editing is acceptable, then you can use:
List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
foos = new List<List<Foo>>{
new List<Foo>(){
new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
},
new List<Foo>() {
new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
},
};
dataGridView1.DataSource = foos.SelectMany(x=>x).ToList();
}
And edit data in a flat list, like this:
When you edit each row, you are in fact editing the original list.

Binding WPF DataGrid to List<Interface>

Hello I have a DataGrid and I have different reports that I want to show. I'm going to change the classes so they are shorter in here but Idea is the same.
Lets say that I Have an Interface called IReports
public interface IReports
{
}
and three classes called Students, Classes, Cars
public class Students:IReports
{
public string Name { get; set; }
}
public class Classes : IReports
{
public string ClassName { get; set; }
public string StudentName { get; set; }
}
public class Cars : IReports
{
public int Mileage { get; set; }
public string CarType { get; set; }
public string StudentName { get; set; }
}
The List
private List<IReports> _reportsTable;
public List<IReports> ReportsTable
{
get { return _reportsTable; }
set { SetProperty(ref (_reportsTable), value); }
}
the DataGrid
<DataGrid ItemsSource="{Binding ReportsList}"
Grid.Column="1"
Grid.Row="0"
AutoGenerateColumns="True"
Grid.RowSpan="6"/>
Okay, so what is important here is they all have different property names and some have more some have less. How can I bind the DataGrid to look at the different properties? This is MVVM if that makes any difference.
Update: What this will always only use one of the classes at a time.but when someone changes a combobox it will fire an event that will fill the IList<IReports>.
What this will always only use one of the classes at a time. but when someone changes a combobox it will fire an event that will fill the IList<IReports>.
The way I understand the above is that you never mix different elements inside the list (i.e. it contains only Classes, Students or Cars). All the other answers are assuming the list contains mixed content, but if that's the true, then DataGrid is simply not the right presenter for such content.
If the above assumption is correct, then the only problem is how to represent different lists with a single bindable property. As can be seen in Data Binding Overview, when dealing with collection, data binding does not really care if they are generic or not. The recognizable source types are the non generic IEnumerable, IList and IBindingList. However, the collection view implementation is using some rules to determine the element type of the collection, by seeking for generic type argument of implemented IEnumerable<T> interfaces by the actual data source class, by checking the first available item, or taking the information from ITypedList implementation etc. All the rules and their precedence can be seen in the Reference Source.
With all that in mind, one possible solution could be to change the ReportsTable property type to allow assigning List<Classes> or List<Students or List<Cars>. Any common class/interface will work (remember, data binding will check the actual type returned by GetType()) like object, IEnumerable, IList, IEnumerable<IReports> etc., so I'll choose the closest covariant type to List<IReports which is IReadOnlyList<IReports>:
private IReadOnlyList<IReports> _reportsTable;
public IReadOnlyList<IReports> ReportsTable
{
get { return _reportsTable; }
set { SetProperty(ref (_reportsTable), value); }
}
Now when you do this
viewModel.ReportsTable = new List<Students>
{
new Students { Name = "A" },
new Students { Name = "B" },
new Students { Name = "C" },
new Students { Name = "D" },
};
you get
while with this
viewModel.ReportsTable = new List<Classes>
{
new Classes { ClassName = "A", StudentName = "A" },
new Classes { ClassName = "A", StudentName ="B" },
new Classes { ClassName = "B", StudentName = "C" },
new Classes { ClassName = "B", StudentName = "D" },
};
it shows
and finally this
viewModel.ReportsTable = new List<Cars>
{
new Cars { Mileage = 100, CarType = "BMW", StudentName = "A" },
new Cars { Mileage = 200, CarType = "BMW", StudentName = "B" },
new Cars { Mileage = 300, CarType = "BMW", StudentName = "C" },
new Cars { Mileage = 400, CarType = "BMW", StudentName = "D" },
};
results in
UPDATE: The above requires modifying the model to return concrete List<T> instances. If you want to keep the model as it is (i.e. returning List<IReports>), then you'll need a different solution, this time utilizing the ITypedList. In order to do that, we'll create a simple list wrapper using the System.Collections.ObjectModel.Collection<T> base class:
public class ReportsList : Collection<IReports>, ITypedList
{
public ReportsList(IList<IReports> source) : base(source) { }
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
return TypeDescriptor.GetProperties(Count > 0 ? this[0].GetType() : typeof(IReports));
}
public string GetListName(PropertyDescriptor[] listAccessors) { return null; }
}
then change the bindable property to
private IList<IReports> _reportsTable;
public IList<IReports> ReportsTable
{
get { return _reportsTable; }
set { SetProperty(ref _reportsTable, value as ReportsList ?? new ReportsList(value)); }
}
and you are done.
As I understand it, you want a datagrid to show the various columns of various classes that implement an interface. If you hook the DataGrid's LoadingRow event, you can see what types of objects you are dealing with at runtime. You can use reflection to get the properties off the row's datacontext and then check the datagrid to see if there is a column for that property. If not, add it.
An issue will be if there are different types in the list and a type doesn't have a property that is in another type (like Cars doesn't have a Name property and both Students and Cars are in the list). If you edit a column for a property that doesn't exist on the object, you'll throw an exception. To get around this, you'll need a converter and style that applies it to the datagridcells. For fun, I also added a datatrigger that changes the background of the cell to Silver if it is disabled. One issue will be if you need to change the cell's style then you have to do it in the code (or change the style in the code to be based on your style).
XAML:
<DataGrid ItemsSource="{Binding ReportsTable}" AutoGenerateColumns="True" LoadingRow="DataGrid_LoadingRow" />
CS
private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
var dg = sender as DataGrid;
var pis = e.Row.DataContext.GetType().GetProperties();
foreach (var pi in pis)
{
// Check if this property already has a column in the datagrid
string name = pi.Name;
var q = dg.Columns.Where(_ => _.SortMemberPath == name);
if (!q.Any())
{
// No column matches, so add one
DataGridTextColumn c = new DataGridTextColumn();
c.Header = name;
c.SortMemberPath = name;
System.Windows.Data.Binding b = new Binding(name);
c.Binding = b;
// All columns don't apply to all items in the list
// So, we need to disable the cells that aren't applicable
// We'll use a converter on the IsEnabled property of the cell
b = new Binding();
b.Converter = new ReadOnlyConverter();
b.ConverterParameter = name;
// Can't apply it directly, so we have to make a style that applies it
Style s = new Style(typeof(DataGridCell));
s.Setters.Add(new Setter(DataGridCell.IsEnabledProperty, b));
// Add a trigger to the style to color the background when disabled
var dt = new DataTrigger() { Binding = b, Value = false };
dt.Setters.Add(new Setter(DataGridCell.BackgroundProperty, Brushes.Silver));
s.Triggers.Add(dt);
c.CellStyle = s;
// Add the column to the datagrid
dg.Columns.Add(c);
}
}
}
CS for the converter:
public class ReadOnlyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
var prop = value.GetType().GetProperty(parameter as string);
if (prop != null)
return true;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And, just to be complete, this is what I used to setup the data for the screenshot:
public List<IReports> ReportsTable { get; set; }
public MainWindow()
{
InitializeComponent();
ReportsTable = new List<IReports>() {
new Students() { Name = "Student 1" },
new Students() { Name = "Student 2" },
new Classes() { ClassName="CS 101", StudentName = "Student 3" },
new Cars() { CarType = "Truck", Mileage=12345, StudentName = "Student 4" }
};
this.DataContext = this;
}
Screenshot:
Instead of a converter option to display a given string value, why not add a getter to the base interface. Then, each class just returns its own, almost like every object can override its "ToString()" method Since you would create a list such as for display, or picking, the value would be read-only anyhow, Make it just a getter...
public interface IReports
{
string ShowValue {get;}
}
public class Students:IReports
{
public string Name { get; set; }
public string ShowValue { get { return Name; } }
}
public class Classes : IReports
{
public string ClassName { get; set; }
public string StudentName { get; set; }
public string ShowValue { get { return ClassName + " - " + StudentName ; } }
}
public class Cars : IReports
{
public int Mileage { get; set; }
public string CarType { get; set; }
public string StudentName { get; set; }
public string ShowValue { get { return CarType + "(" + Mileage + ") - " + StudentName; } }
}
Then in your view model manager...
public class YourMVVMClass
{
public YourMVVMClass()
{
SelectedRptRow = null;
ReportsTable = new List<IReports>()
{
new Students() { Name = "Student 1" },
new Students() { Name = "Student 2" },
new Classes() { ClassName="CS 101", StudentName = "Student 3" },
new Cars() { CarType = "Truck", Mileage=12345, StudentName = "Student 4" }
};
}
// This get/set for binding your data grid to
public List<IReports> ReportsTable { get; set; }
// This for the Selected Row the data grid binds to
public IReports SelectedRptRow { get; set; }
// This for a user double-clicking to select an entry from
private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Now, you can look directly at the SelectedRptRow
// as in the data-grid binding declaration.
if (SelectedRptRow is Classes)
MessageBox.Show("User selected a class item");
else if( SelectedRptRow is Cars)
MessageBox.Show("User selected a car item");
else if( SelectedRptRow is Students)
MessageBox.Show("User selected a student item");
else
MessageBox.Show("No entry selected");
}
}
Finally in your form/view
<DataGrid Grid.Row="0" Grid.Column="0"
ItemsSource="{Binding ReportsTable}"
SelectedItem="{Binding SelectedRptRow}"
MouseDoubleClick="Control_OnMouseDoubleClick"
AutoGenerateColumns="False"
Width="200" Height ="140"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<DataGrid.Columns>
<DataGridTextColumn
Header="Report Item"
Width="180"
IsReadOnly="True"
CanUserSort="False"
Binding="{Binding Path=ShowValue}" />
</DataGrid.Columns>
</DataGrid>
The other answers using the converters are just another path, but this avenue to me is easier as you can change each individual class and expand / adjust as needed. The exposed "ShowValue" getter is common to all instances of the "IReports", so the binding is direct without going through the converter. If you remove a class, or extend in the future, your underlying is all self-contained.
Now don't get me wrong, I do use converters and typically do with Boolean type fields to respectively show, hide, collapse controls as needed. This is nice as I have different boolean converters such as
BoolToVisibleHidden = if True, make visible vs Hidden
BoolToHiddenVisible = if True, make Hidden vs Visible
BoolToVisibleCollapse = if True, make visible vs Collapsed
BoolToCollapseVisible = if True, make Collapsed vs visible.
So, with one boolean property on my MVVM, I can both show AND hide different controls... maybe such as an admin vs standard user option.
I've also used converters dealing with dates for alternate formatting purposes.
You could abuse IValueConverter for that. Create one for each column.
In the ValueConverter you can test for the type and return the correct property. An example of what I mean:
public class NameValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Students)
{
return (value as Students).Name;
}
if (value is Classes)
{
return (value as Classes).ClassName;
}
if (value is Cars)
{
return (value as Cars).CarType;
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
To use it add it as resource to the DataGrid:
<DataGrid.Resources>
<local:NameValueConverter x:Key="NameValueConverter"></local:NameValueConverter>
</DataGrid.Resources>
And specify it in the binding like this:
{Binding Path=., Converter={StaticResource NameValueConverter}}
This solution would only work for read-only DataGrids though (editing throws a NotImplementedException).

Bound DataGridView: the DataGridViewComboBoxColumn changes to a different value when I click outside it

I hope someone can help me out with an obscure problem that has me absolutely flummoxed.
I wish to set up a DataGridView, which allows a user to select an option from a DataGridViewComboBoxColumn, and for the object which is a datasource for the DataGridView, to be updated with the object that the user pick in the ComboBox.
[NB I wish the DataGridViewComboBoxColumn to show more than a single property in the dropdown, hence why I am using a DataTable as the DataGridViewComboBoxColumn datasource. In other words, I want them to see a description which is a combination of other properties concatenated together.]
My code works, but when I click outside the ComboBox cell, the value gets automatically set (it gets set back to the first item in BindingList). I cannot get it to stick to the value that the user selected. Ultimately the wrong object instance gets assigned to the DataGridView's dataSource.
I would post a pic, but I don't have enough rep.
So my question is, why does the DataGridViewComboBoxColumn switch to the first item in its datasource (a DataTable) when I click outside the cell. How can I get it to keep the option I selected, in the cell. I am absolutely BAFFLED.
I hope it's okay to post this much code up to the StackOverflow website without it annoying everyone. I've tried to create suitably generic example to help any other souls trying to find out how to select and assign an object to the property of another object, using a DataGridViewComboBoxColumn. So hopefully this is of use to someone else. I've also tried to make it relatively easy to recreate, should someone need to solve this kind of problem.
I've tried all manner of things, including using a List as a datasource of the DataGridViewComboBoxColumn, doing things with the CurrentCellDirtyStateChanged event - all to no avail.
Here's my 2 classes:
public class CarPartChoice
{
public string Name { get; set; }
public int Value { get; set; }
public string Comment {get; set;}
public CarPartChoice(string name, int value, string comment)
{
Name = name;
Value = value;
Comment = comment;
}
}
public class Car
{
public string Manufacturer { get; set; }
public string Model { get; set; }
public CarPartChoice WheelChoice {get; set;}
public Car(string maker, string model, CarPartChoice wheel)
{
Manufacturer = maker;
Model = model;
WheelChoice = wheel;
}
public Car(string maker, string model)
{
Manufacturer = maker;
Model = model;
}
}
public static class GlobalVariables
{
public static BindingList<CarPartChoice> GlobalChoiceList { get; set; }
public static BindingList<Car> GlobalCarsList { get; set; }
}
And here's me creating my dataGridView:
private void Form1_Load(object sender, EventArgs e)
{
//Setup the wheel choices to be selected from the DataGridViewComboBoxColumn.
CarPartChoice myWheelChoice = new CarPartChoice("Chrome", 19, "This is the chromes wheels option.");
CarPartChoice myWheelChoice2 = new CarPartChoice("HubCaps", 16, "This is the nasty plastic hubcaps option.");
BindingList<CarPartChoice> tempBLChoice = new BindingList<CarPartChoice>();
tempBLChoice.Add(myWheelChoice);
tempBLChoice.Add(myWheelChoice2);
GlobalVariables.GlobalChoiceList = tempBLChoice;
//Setup the cars to populate the datagridview.
Car car1 = new Car("Vauxhall", "Astra");
Car car2 = new Car("Mercedes", "S-class");
BindingList<Car> tempListCars = new BindingList<Car>();
tempListCars.Add(car1);
tempListCars.Add(car2);
GlobalVariables.GlobalCarsList = tempListCars;
dataGridView1.AutoGenerateColumns = false;
dataGridView1.CurrentCellDirtyStateChanged += new EventHandler(dataGridView1_CurrentCellDirtyStateChanged);
// Set up 2 DataGridViewTextBox columns, one to show the manufacturer and the other to show the model.
DataGridViewTextBoxColumn manufacturer_col = new DataGridViewTextBoxColumn();
manufacturer_col.DataPropertyName = "Manufacturer";
manufacturer_col.Name = "Manufacturer";
manufacturer_col.HeaderText = "Manufacturer";
DataGridViewTextBoxColumn model_col = new DataGridViewTextBoxColumn();
model_col.DataPropertyName = "Model";
model_col.Name = "Model";
model_col.HeaderText = "Model";
// Create a DataTable to hold the Wheel options available for the user to choose from. This DT will be the DataSource for the
// ...combobox column
DataTable wheelChoices = new DataTable();
DataColumn choice = new DataColumn("Choice", typeof(CarPartChoice));
DataColumn choiceDescription = new DataColumn("Description", typeof(String));
wheelChoices.Columns.Add(choice);
wheelChoices.Columns.Add(choiceDescription);
foreach (CarPartChoice wheelchoice in GlobalVariables.GlobalChoiceList)
{
wheelChoices.Rows.Add(wheelchoice, wheelchoice.Name + " - " + wheelchoice.Value.ToString() + " - " + wheelchoice.Comment);
}
// Create the Combobox column, populated with the wheel options so that user can pick one.
DataGridViewComboBoxColumn wheelOption_col = new DataGridViewComboBoxColumn();
wheelOption_col.DataPropertyName = "WheelChoice";
wheelOption_col.Name = "WheelChoice";
wheelOption_col.DataSource = wheelChoices;
wheelOption_col.ValueMember = "Choice";
wheelOption_col.DisplayMember = "Description";
wheelOption_col.ValueType = typeof(CarPartChoice);
// Add the columns and set the datasource for the DGV.
dataGridView1.Columns.Add(manufacturer_col);
dataGridView1.Columns.Add(model_col);
dataGridView1.Columns.Add(wheelOption_col);
dataGridView1.DataSource = GlobalVariables.GlobalCarsList;
}
UPDATE:
I have tried to use a "BindingSource" object and setting the datasource of the BindingSource to the DataTable. That made no difference. I've also tried getting the Car to implement the "INotifyPropertyChanged" interface and that didn't make any difference.
The one thing if I've noticed is that the Car item in the datagridview DOES get its WheelChoice property updated. The Car objects IS INDEED updated with the value I've selected from the ComboBox. I think this is a just a display issue and the DataGridViewComboBoxColumn just isn't populated with the correct value. Still baffled.
Thanks to those who left comments. I found the answer in the end.
The answer is for me simply to define an overridden "ToString()" method for my "CarPartChoice" class (i.e. the class which is used to provide the items to be looked-up by clicking on the ComboBoxColumn). I guess it was having trouble formatting my cell with a nice string when I moved control away from it.
So, for anyone who ever wants to do this in future, here's a full example of how you might use a datagridview to update a list of objects of a certain class, and using a DataGridViewComboBoxColumn to provide the user with a choice of objects, and for their selected object (which they chose from the dropdown), to be populated into the list (to be precise: in the field of an object in the list which is of the type of object which gets selected by the user in the dropdown). Yes, I know that's a very horrible sentence.
Here's the complete code which would accomplish this task (I would have thought it was something that people would often want to do).
Kindest regards to all.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//Setup the wheel choices to be selected from the DataGridViewComboBoxColumn.
CarPartChoice myWheelChoice = new CarPartChoice("Chrome", 19, "This is the chromes wheels option.");
CarPartChoice myWheelChoice2 = new CarPartChoice("HubCaps", 16, "This is the nasty plastic hubcaps option.");
CarPartChoice myWheelChoice3 = new CarPartChoice("Iron", 15, "These are metal wheels.");
CarPartChoice myWheelChoice4 = new CarPartChoice("Spoked", 15, "This is the fancy classic hubcaps option.");
CarPartChoice myWheelChoice5 = new CarPartChoice("solid", 13, "This wheels has no spokes or holes.");
CarPartChoice myWheelChoice6 = new CarPartChoice("SpaceHubCaps", 17, "Newly developed space hubcaps.");
BindingList<CarPartChoice> tempBLChoice = new BindingList<CarPartChoice>();
tempBLChoice.Add(myWheelChoice);
tempBLChoice.Add(myWheelChoice2);
tempBLChoice.Add(myWheelChoice3);
tempBLChoice.Add(myWheelChoice4);
tempBLChoice.Add(myWheelChoice5);
tempBLChoice.Add(myWheelChoice6);
GlobalVariables.GlobalChoiceList = tempBLChoice;
//Setup the cars to populate the datagridview.
Car car1 = new Car("Vauxhall", "Astra");
Car car2 = new Car("Mercedes", "S-class");
BindingList<Car> tempListCars = new BindingList<Car>();
tempListCars.Add(car1);
tempListCars.Add(car2);
GlobalVariables.GlobalCarsList = tempListCars;
dataGridView1.AutoGenerateColumns = false;
dataGridView1.CurrentCellDirtyStateChanged += new EventHandler(dataGridView1_CurrentCellDirtyStateChanged);
// Set up 2 DataGridViewTextBox columns, one to show the manufacturer and the other to show the model.
DataGridViewTextBoxColumn manufacturer_col = new DataGridViewTextBoxColumn();
manufacturer_col.DataPropertyName = "Manufacturer";
manufacturer_col.Name = "Manufacturer";
manufacturer_col.HeaderText = "Manufacturer";
DataGridViewTextBoxColumn model_col = new DataGridViewTextBoxColumn();
model_col.DataPropertyName = "Model";
model_col.Name = "Model";
model_col.HeaderText = "Model";
// Create a DataTable to hold the Wheel options available for the user to choose from. This DT will be the DataSource for the
// ...combobox column
DataTable wheelChoices = new DataTable();
DataColumn choice = new DataColumn("Choice", typeof(CarPartChoice));
DataColumn choiceDescription = new DataColumn("Description", typeof(String));
wheelChoices.Columns.Add(choice);
wheelChoices.Columns.Add(choiceDescription);
foreach (CarPartChoice wheelchoice in GlobalVariables.GlobalChoiceList)
{
wheelChoices.Rows.Add(wheelchoice, wheelchoice.Name + " - " + wheelchoice.Value.ToString() + " - " + wheelchoice.Comment);
}
// Create the Combobox column, populated with the wheel options so that user can pick one.
DataGridViewComboBoxColumn wheelOption_col = new DataGridViewComboBoxColumn();
wheelOption_col.DataPropertyName = "WheelChoice";
wheelOption_col.DataSource = wheelChoices;
wheelOption_col.ValueMember = "Choice";
wheelOption_col.DisplayMember = "Description";
wheelOption_col.ValueType = typeof(CarPartChoice);
// Add the columns and set the datasource for the DGV.
dataGridView1.Columns.Add(manufacturer_col);
dataGridView1.Columns.Add(model_col);
dataGridView1.Columns.Add(wheelOption_col);
dataGridView1.DataSource = GlobalVariables.GlobalCarsList;
}
void dataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
var grid = sender as DataGridView;
if (grid.IsCurrentCellDirty)
grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
public class CarPartChoice
{
public string Name { get; set; }
public int Value { get; set; }
public string Comment { get; set; }
public CarPartChoice(string name, int value, string comment)
{
Name = name;
Value = value;
Comment = comment;
}
public override string ToString()
{
return Name.ToString() + " - " + Value.ToString() + " - " + Comment.ToString();
}
}
public class Car
{
public string Manufacturer { get; set; }
public string Model {get; set; }
public CarPartChoice WheelChoice { get; set; }
public Car(string maker, string model, CarPartChoice wheel)
{
Manufacturer = maker;
Model = model;
WheelChoice = wheel;
}
public Car(string maker, string model)
{
Manufacturer = maker;
Model = model;
}
}
public static class GlobalVariables
{
public static BindingList<CarPartChoice> GlobalChoiceList { get; set; }
public static BindingList<Car> GlobalCarsList { get; set; }
}
I too had the same problem but with a different dataset. I was trying to add a keyvalue pair to the combobox and everytime I click outside, it got reset to the first item. What I was missing was that I hadn't set displayMember property of the combobox.
(Hope this would help if any others are here for the same problem)

Convert String in to Enum Without knowing it's type in C#

I am facing a problem. I have set of some enum in my app. Like
public enum EnmSection
{
Section1,
Section2,
Section3
}
public enum Section1
{
TestA,
TestB
}
public enum Section2
{
Test1,
Test2
}
EnmSection is main enum which contains the other enum(as string) which are declared below it. Now i have to fill the values of EnmSection in a drop-down.I have done it.
Like this...
drpSectionType.DataSource = Enum.GetNames(typeof(EnmSection));
drpSectionType.DataBind();
Now my drop-down has values: Section1,Section2,Section3
Problem is:
I have another drop-down drpSubSection. Now i want to fill this drop-down whatever i have selected in the drpSectionType.
for ex If I selected Section1 in drpSectionType then drpSubsection should contain the value
TestA,TestB. Like this:
protected void drpSectionType_SelectedIndexChanged(object sender, EventArgs e)
{
string strType = drpSectionType.SelectedValue;
drpSubsection.DataSource = Enum.GetNames(typeof());
drpSubsection.DataBind();
}
Here typeof() is expecting the enum.But i am getting selected value as string. How can i achieve this functionality.
Thanks
What if you reference an assembly that contains another enum with a value named Section1?
You'll just have to try all the enums you care about, one at a time, and see which one works. You'll probably want to use Enum.TryParse.
Something like this might work, but you have to do some exception handling:
protected void drpSectionType_SelectedIndexChanged(object sender, EventArgs e)
{
string strType = drpSectionType.SelectedValue;
EnmSection section = (EnmSection)Enum.Parse(typeof(EnmSection), strType);
drpSubsection.DataSource = Enum.GetNames(typeof(section));
drpSubsection.DataBind();
}
This might be a bit over the top but it would work if you bind bind Arrays of IEnumItem to your drop down and set it up to show their display text.
public interface IEnumBase
{
IEnumItem[] Items { get; }
}
public interface IEnumItem : IEnumBase
{
string DisplayText { get; }
}
public class EnumItem : IEnumItem
{
public string DisplayText { get; set; }
public IEnumItem[] Items { get; set; }
}
public class EnmSections : IEnumBase
{
public IEnumItem[] Items { get; private set; }
public EnmSections()
{
Items = new IEnumItem[]
{
new EnumItem
{
DisplayText = "Section1",
Items = new IEnumItem[]
{
new EnumItem { DisplayText = "TestA" },
new EnumItem { DisplayText = "TestB" }
}
},
new EnumItem
{
DisplayText = "Section2",
Items = new IEnumItem[]
{
new EnumItem { DisplayText = "Test1" },
new EnumItem { DisplayText = "Test2" }
}
}
};
}
}
drpSubsection.DataSource = Enum.GetNames(Type.GetType("Your.Namespace." + strType));
If the enums are in another assembly, (i.e. they're not in mscorlib or the current assembly) you'll need to provide the AssemblyQualifiedName. The easiest way to get this will be to look at typeof(Section1).AssemblyQualifiedName, then modify your code to include all the necessary parts. The code will look something like this when you're done:
drpSubsection.DataSource = Enum.GetNames(Type.GetType("Your.Namespace." + strType + ", MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"));

Programmatically create columns in a View

This should be easy. I want to populate a grid with a custom data source at runtime. For some reason, it simply does not work.
Running via a unit test
[TestMethod]
public void Runtest() {
TestForm form = new TestForm();
TestControl control = new TestControl();
form.Controls.Add(control);
control.LoadData();
form.ShowDialog();
}
The relevant Control code
public void LoadData() {
SourceRecord[] original = new SourceRecord[] {
new SourceRecord("1"), new SourceRecord("3"), new SourceRecord("9") };
gridControl1.DataSource = original;
GridColumn col = gridView1.Columns.AddVisible("SomeColumn");
col.FieldName = "SomeName";
//gridControl1.ForceInitialize();
}
Record info
public class SourceRecord {
public string SomeName = "";
public SourceRecord(string Name) {
this.SomeName = Name;
}
}
I end up with some column just called "Column" which displays 3 rows reading ClassLibrary1.SourceRecord. Then my custom column "Some Name" has no data. According to the devexpress walkthrough I only need to populate the DataSource with a class that implements IList, which I did with an Array.
How can I display just my custom column and give it the data?
The grid control will bind columns to properties only. Try this:
public class SourceRecord
{
public string SomeName { get; set; }
public SourceRecord(string Name)
{
SomeName = Name;
}
}
public void LoadData()
{
SourceRecord[] original = new SourceRecord[] { new SourceRecord("1"), new SourceRecord("3"), new SourceRecord("9") };
GridColumn col = gridView1.Columns.AddVisible("SomeColumn");
col.FieldName = "SomeName";
gridControl1.DataSource = original;
}

Categories