I'm using a DataGridView binding its datasource to a List, and specifying the properties for each column.
An example would be:
DataGridViewTextBoxColumn colConcept = new DataGridViewTextBoxColumn();
DataGridViewCell cell4 = new DataGridViewTextBoxCell();
colConcept.CellTemplate = cell4;
colConcept.Name = "concept";
colConcept.HeaderText = "Concept";
colConcept.DataPropertyName = "Concept";
colConcept.Width = 200;
this.dataGridViewBills.Columns.Add(colConcept);
{... assign other colums...}
And finally
this.dataGridViewBills.DataSource=billslist; //billslist is List<Bill>
Obviously Class Bill has a Property called Concept, as well as one Property for each column.
Well, now my problem, is that Bill should have and Array/List/whateverdynamicsizecontainer of strings called Years.
Let's assume that every Bill will have the same Years.Count, but this only known at runtime.Thus, I can't specify properties like Bill.FirstYear to obtain Bill.Years[0], Bill.SecondYear to obtain Bills.Years[1]... etc... and bind it to each column.
The idea, is that now I want to have a grid with dynamic number of colums (known at runtime), and each column filled with a string from the Bill.Years List. I can make a loop to add columns to the grid at runtime depending of Bill.Years.Count, but is possible to bind them to each of the strings that the Bill.Years List contains???
I'm not sure if I'm clear enough.
The result ideally would be something like this, for 2 bills on the list, and 3 years for each bill:
--------------------------------------GRID HEADER-------------------------------
NAME CONCEPT YEAR1 YEAR2 YEAR3
--------------------------------------GRID VALUES-------------------------------
Bill1 Bill1.Concept Bill1.Years[0] Bill1.Years[1] Bill1.Years[2]
Bill2 Bill2.Concept Bill2.Years[0] Bill2.Years[1] Bill2.Years[2]
I can always forget the datasource, and write each cell manually, as the MSFlexGrid used to like, but if possible, I would like to use the binding capabilities of the DataGridView.
Any ideas? Thanks a lot.
I recently ran into this same problem. I ended up using DataGridView's virtual mode instead of binding to a data source. It doesn't have exactly the same features as binding, but it's still a lot more powerful than populating each cell manually.
In virtual mode, the DataGridView will fire an event whenever it needs to display a cell, which essentially means you can populate the cell however you please:
private void my_init_function() {
datagridview.VirtualMode = true;
datagridview.CellValueNeeded += new System.Windows.Forms.DataGridViewCellValueEventHandler(datagridview_CellValueNeeded);
}
private void datagridview_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
e.Value = get_my_data(e.RowIndex, e.ColumnIndex);
}
You could use reflection to set up and fill the DataGridView. I've done this with a single type, but I don't see why it couldn't be extended to your data structure.
To set up the DataGridView:
// Create the columns based on the data in the album info - get by reflection
var ai = new AlbumInfo();
Type t = ai.GetType();
dataTable.TableName = t.Name;
foreach (PropertyInfo p in t.GetProperties())
{
// IF TYPE IS AN ARRAY (OR LIST) THEN ADD A COLUMN FOR EACH ELEMENT
var columnSpec = new DataColumn();
// If nullable get the underlying type
Type propertyType = p.PropertyType;
if (IsNullableType(propertyType))
{
var nc = new NullableConverter(propertyType);
propertyType = nc.UnderlyingType;
}
columnSpec.DataType = propertyType;
columnSpec.ColumnName = p.Name;
dataTable.Columns.Add(columnSpec);
}
dataGridView.DataSource = dataTable;
Then to populate the DataGridView:
// Add album info to table - add by reflection
var ai = new AlbumInfo();
Type t = ai.GetType();
// WOULD NEED TO INCREASE BY LENGTH OF ARRAY
var row = new object[t.GetProperties().Length];
int index = 0;
foreach (PropertyInfo p in t.GetProperties())
{
// IF TYPE IS AN ARRAY (OR LIST) THEN ADD EACH ELEMENT
row[index++] = p.GetValue(info, null);
}
dataTable.Rows.Add(row);
This is just the code I used, so you'll have to modify the code to handle your year array/list.
Related
I'm using a DataGrid and programmatically generating the columns with code-behind.
The columns are derived from DataGridTextColumn with some additions for locking key input to a specific type.
I need the columns to sort numerically:
1
2
10
11
instead of the default string based sort:
1
10
11
2
I've tried a DataGridSortingEvent, but it fails to cast from BindingListCollectionView to IList
ListCollectionView lcv = new ListCollectionView((IList)CollectionViewSource.GetDefaultView(dataGrid.ItemsSource));
Here's how I'm creating the DataGrid
DataSet set = new DataSet();
set.ReadXml(xmlDocument.CreateReader());
DataView data = set.Tables["row"];
dataGrid.ItemsSource = data;
Here's the block where I'm creating the columns
dataGrid.Columns.Add(new DataChimpIntegerColumn()
{
Header = column.ColumnTitle,
Binding = new Binding(column.ColumnTitle),
MaxWidth = 150,
DefaultValue = column.ColumnDefault,
});
And the event block
private void dataGrid_Sorting(object sender, DataGridSortingEventArgs e)
{
DataGridColumn column = e.Column;
IComparer comparer = null;
if (e.Column.SortMemberPath != "id") return;
e.Handled = true;
ListSortDirection direction = (column.SortDirection != ListSortDirection.Ascending)
? ListSortDirection.Ascending : ListSortDirection.Descending;
column.SortDirection = direction;
// Error -->
ListCollectionView lcv = new ListCollectionView((IList)CollectionViewSource
.GetDefaultView(dataGrid.ItemsSource));
comparer = new SortNumerical(direction);
lcv.CustomSort = comparer;
}
The exact error message is:
System.InvalidCastException: 'Unable to cast object of type 'System.Windows.Data.BindingListCollectionView' to type 'System.Collections.IList'.'
Found a solution that works.
Changing the ItemsSource type to ObservableCollection<ExpandoObject> allowed me to dynamically create classes according to the XML file loaded (thanks C# 6).
I could then directly use ListCollectionView lcv = new ListCollectionView(xmlData.ItemsSource as IList);
Answer
During creation of the columns you should be able to specify the SortMemberPath because your custom column is deriving from DataGridTextColumn. However this does mean you have to have access to the integer representation of the value you want to sort by.
In my test I came up with this programmatic initialization of the columns:
this.deviceLogDataGrid.Columns.Add(new DataGridTextColumn()
{
Header = "Error Number",
Binding = new Binding(nameof(DeviceLogRowVM.ErrorNumberString)),
MaxWidth = 150,
// DefaultValue = column.ColumnDefault,
SortMemberPath = nameof(DeviceLogRowVM.ErrorNumber)
});
The property ErrorNumber is the actual integer value and the ErrorNumberString is the string representation of the value. I use the nameof expression in order to make refactoring names of properties more safe.
Alternatively
If you just want to use the string representation to format the text correctly you could try to use the StringFormat property on the Binding object instead. Your binding initialization could look like this:
Binding = new Binding(nameof(DeviceLogRowVM.ErrorNumber)) { StringFormat = "ER-{0}" },
This will also by default (without the use of the SortMemberPath property) sort the column correctly as the bound value is actually an integer which just get formatted before displaying it. Useful if you only want to show 2 decimal digits of a float but needs to sort by the full precision.
I'm trying to bind a DataGridView to a reflected object array. The header columns bind fine, the correct name is displayed and seven rows are displayed, the issue is the rows are empty.
When I check the databound items it looks fine.
It shows that it's the correct reflected model and the values.
This is the snippet I've got so far.
private void comboBoxTables_SelectedIndexChanged(object sender, EventArgs e)
{
var type = (Type)(this.comboBoxTables.SelectedItem as ComboBoxItem).Value;
object[] result = this.DataLoader.Get(type);
dataGridView1.DataSource = result;
this.dataGridView1.Columns.Clear();
var properties = type.GetProperties();
foreach (var property in properties)
{
this.dataGridView1.Columns.Add(property.Name, property.Name);
this.dataGridView1.Columns[property.Name].DataPropertyName = property.Name;
}
this.dataGridView1.Refresh();
}
This snippet:
object[] result = this.DataLoader.Get(type);
Fetches the data from a dictionary containing the reflected values as an object array.
I've tried using a binding source instead and some other ugly hacks, but I can't get the rows to display any data.
Any help is greatly appreciated, thank you in advance.
SOLVED
Not sure why this solved the issue, but by adding a ToList() on the result, the data was displayed correctly. It might be because of an un-enumerated IEnumerable earlier in the code.
dataGridView1.DataSource = result.ToList();
SOLVED
Not sure why this solved the issue, but by adding a ToList() on the result, the data was displayed correctly. I'll fill in the blanks once I know why this is the case.
dataGridView1.DataSource = result.ToList();
I tried to recreate your code and the real problem, in my opinion, is the
var properties = type.GetProperties();
not being a Property, ironically. Meaning they don't have "get{}, set{}" as normal properties.
My solution is to make an external class to work as a "shell" for the properties you get from your reflection:
public class Shell
{
public string Name { get; private set; }
public Shell(string name)
{
Name = name;
}
}
and something along these lines:
var type = (Type)(this.comboBoxTables.SelectedItem as ComboBoxItem).Value;
object[] result = this.DataLoader.Get(type);
//this.dataGridView1.Columns.Clear();
var properties = type.GetProperties();
List<Shell> shells = new List<Shell>();
foreach (var item in properties)
{
shells.Add(new Shell(item.Name));
}
dataGridView1.DataSource = shells;
foreach (var property in shells)
{
this.dataGridView1.Columns.Add(property.Name, property.Name);
this.dataGridView1.Columns[property.Name].DataPropertyName = property.Name;
}
this.dataGridView1.Refresh();
Edit: Forgot to change datagrid datasource to the newly created list of shells
I have a DataGridView that I am binding to the Values collection of a dictionary. In my form's constructor, I can create the dictionary and bind the columns of the DataGridView to fields in the structure that will be contained in the dictionary:
m_tasks = new Dictionary<int,OperationsTaskLabeledData>();
var taskArray = from task in m_tasks.Values select new {
StartDate = task.m_start_task_date.ToString("M/dd H:mm"),
EndDate = task.m_task_date.ToString("M/dd H:mm"),
Description = task.m_short_desc,
Object = task.m_device_id,
InitialLocation = task.m_initial_location,
FinalLocation = task.m_final_location };
dgvFutureTasks.DataSource = taskArray.ToArray();
I want this code in the form's constructor so that the columns can be formatted, and I won't have to reformat them every time data in the grid is updated.
When I actually have data to display in this datagridview, what do I do with it? I will call this function:
private void DisplayFutureTasks(IEnumerable<OperationsTaskLabeledData> tasks)
But I don't know what to do inside this function.
I can just re-bind the datagridview control and reformat all of the columns each time the control is updated, but that seems very wasteful. I'm sure there's a better way to do it, and I'd much rather do this in some reasonable fashion instead of using ugly brute force.
I have now figured out how to do what I want to do.
I know the columns I will need at design time, so in the IDE I add the columns to my datagridview and format them as desired. I then set the AutoGenerateColumns property of the grid view to false. For some unknown reason, that property is not available in the designer and has to be set in code. Finally, I can set the DataPropertyName of each column to the name of the corresponding field in the structure I will be linking to. For example, here is the LINQ code I will be using to generate the data source:
taskArray = from task in tasks select new {
StartDate = task.m_start_task_date.ToString("M/dd H:mm"),
EndDate = task.m_task_date.ToString("M/dd H:mm"),
Description = task.m_short_desc,
Object = task.m_device_id,
InitialLocation = task.m_initial_location,
FinalLocation = task.m_final_location };
.DataSource = taskArray.ToArray();
And here is the code in my form's constructor to set the DataPropertyName properties:
dgvFutureTasks.AutoGenerateColumns = false;
dgvFutureTasks.Columns["colStartTime"].DataPropertyName = "StartDate";
dgvFutureTasks.Columns["colFinishTime"].DataPropertyName = "EndDate";
dgvFutureTasks.Columns["colDescription"].DataPropertyName = "Description";
dgvFutureTasks.Columns["colObject"].DataPropertyName = "Object";
dgvFutureTasks.Columns["colInitialLocation"].DataPropertyName = "InitialLocation";
dgvFutureTasks.Columns["colFinalLocation"].DataPropertyName = "FinalLocation";
At this point, the DataGridView displayed the data as expected.
RobR
Situation:
I am attempting to bind a BindingList<string[]> constructed from a LINQ to SQL query to a DataGridView.
Problem:
I either cannot make modification to the DataGridView after items are generated -or- I get a bunch of unwanted fields in my DataGridView (it depends on which iteration of my code I use) I have googled as hard as I can and tried implementing most of the solutions I have found online to no avail.
I know that string has no public property for its actual value. I am having a difficult time determining how to retrieve that (I believe is part of the problem).
C#
int item = (from p in CurrentConversion.Companies[lbCompanies.SelectedIndex].Modules
where p.ModuleName.Equals(clbModules.SelectedItem)
select p.ModuleId)
.FirstOrDefault();
BindingList<string[]> Data = new BindingList<string[]>((
from p in CurrentConversion.Companies[lbCompanies.SelectedIndex].QuestionAnswers
where p[2].Equals(item)
select new string[] { p[0].ToString(), p[3].ToString() })
.ToList());
dgvQuestions.DataSource = Data;
dgvQuestions.Refresh();
Unwanted Behavior:
This occurs after binding
Question:
Why is this happening?
How do I fix it?
Additional Information:
I am not sure what additional information may be need but I will supply what is requested.
Also if I switch to my other code iteration:
int item = (from p in CurrentConversion.Companies[lbCompanies.SelectedIndex].Modules where p.ModuleName.Equals(clbModules.SelectedItem) select p.ModuleId).FirstOrDefault();
var Data = new BindingList<object>((from p in CurrentConversion.Companies[lbCompanies.SelectedIndex].QuestionAnswers where p[2].Equals(item) select new {Question = p[0].ToString(), Answer = p[3].ToString() }).Cast<object>().ToList());
dgvQuestions.DataSource = Data;
dgvQuestions.Refresh();
dgvQuestions.Columns[1].ReadOnly = false;
I can see the data properly but I cannot edit the column I would like to.
You are binding to a list of string arrays, and you are getting the properties form the array. Most likely you want something like the following:
var Data = new BindingList<object>((
from p in CurrentConversion.Companies[lbCompanies.SelectedIndex].QuestionAnswers
where p[2].Equals(item)
select new {
Val1 = p[0].ToString(),
Val2 = p[3].ToString()
}).ToList());
The reason you're seeing those fields in the Grid is that you're binding each row to a string[]. So it is automatically displaying the properties of string[] as the columns. There is no built-in logic for the grid to parse an array and use the contents of the array as columns.
In order to get the DataGrid to display your data correctly, you should bind it to a custom type, and it will use the public properties of the type as columns.
I have a gridview that I populate with values I get from a powershell command. For example my powershell command is get-command. I know the command returns the values. Here is my code however my gridview never shows the data.
ArrayList boxesarray = new ArrayList();
foreach (PSObject ps in commandResults)
boxesarray.Add(ps.Properties["Name"].Value.ToString());
boxes.DataSource = boxesarray;
boxes.DataBind();
I know the value is there because I replaced the last two lines with a label and was able to see the value.
boxlabel.text = boxesarray[4];
I must be missing something. Help please.
The GridView requires a collection or IEnumerable of classes which have properties, and the properties are mapped to columns.
An array like yours have value typed objects (strings) which has no roperties, so you can't bind the properties to the columns.
ArrayList boxesarray = new ArrayList();
You could create a simple class like this:
public class PropertyContainer
{
public string Value {get;set;}
}
// NOTE: you can override ToString(); to customize String.Format behaviour
// and to show it in the debugger (althought there's other way for this, using
// DebuggerDisplayAttribute)
And create and populate an array of this class, which will be correctly bound to the datagrid.
foreach (PSObject ps in commandResults)
boxesarray.Add(
new PropertyContainer { Value = ps.Properties["Name"].Value.ToString()});
boxes.DataSource = boxesarray;
boxes.DataBind();
Other option is to convert your array to an array of objects using LINQ. You can even use anonymous object if the data grid columns are set to be automatically created.
// anonymous type
var dataForBinding = boxesArray.select(val => new {Value = val});
// array of the class
var dataForBinding = boxesArray.select(val => new PropertyContainer
{ Value = val });
You can bind this data to your gridview, and it will work perfectly.
You can try
.DataSource = (from ps in commandResults
select { Name:ps.Properties["Name"].Value.ToString() }).ToList();
Or
.DataSource = (from name in yourarraylist
select { Name:name.ToString() }).ToList();