I have a DataTable I want to use as a DataSource of a DataGridView, but with a twist: for reasons (below) I need to exclude a column from DataGridView (not just hide it, truly exclude), ideally preventing it from being generated alltogether. Theoretically, I can call Columns.RemoveAt at an appropriate moment (which is the DataBindingComplete event handler - docs), but that's too late for me (for reasons).
An obvious solution is to set AutoGenerateColumns = false and do it manually. Without having looked into the details of this, I fear I'd need to reinvent the wheel in this scenario (to keep the data bindings working etc).
My reasons for this whole esoteric are, there is huge legacy app, originally written in VB6, and there any byte-array column is just ignored by the MS Hierarchical Grid. I'm trying to emulate this behavior in a custom control derived from DataGridView, and most stuff works.
UPDATE/ANSWER
Just set dt.Columns[0].ColumnMapping = MappingType.Hidden; (courtesy of https://stackoverflow.com/a/31282356/5263865)
In modern programming, there is a tendency to separate your data (=Model) from how the data is shown to the operator (=View). An adapter class (=ViewModel) is needed to connect the Model to the View. Abbreviated this gives MVVM. If you are not familiar with this concept of separation, consider to do some background reading.
Your Data is in a DataTable. You didn't mention what kind of items are in the DataTable. To ease the discussion I'll assume that the DataTable contains a sequence of Products.
class Product
{
...
}
You have methods to put Products in the DataTable and to Access them. Something like:
interface IProductRepository
{
IEnumerable<Product> AllProducts {get;}
Product GetProductById(int productId);
void AddProductAtEnd(Product product);
void ReplaceProduct(Product product, int index);
...
}
etc. The exact methods are not important for the answer. What I try to explain is that when using this interface you hide that the Products are stored in a DataTable. This give you the freedom to change where your Products are stored: in a DataBase? A List? or maybe a file, or even the internet.
I use a generic term repository (warehouse) for something where you can store items, and later retrieve them, replace them with other items or remove them from the repository. This can be a DataTable, or a database, or maybe a file, a Dictionary, whatever. The nice thing is that I've hidden that the Products are in a DataTable.
The DataGridView
When accessing the data in a DataGridView, people tend to fiddle directly with the DataGridViewCells and DataGridViewRows.
Well, don't!
Use DataBinding instead.
In almost all forms that have DataGridViews I have the following properties:
BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.DataGridView1.DataSource;
set => this.DataGridView1.DataSource = value;
}
Product CurrentProduct => this.DataGridView1.CurrentRow as Product;
IEnumerable<Product> SelectedProducts => this.DataGridView1.SelectedRows
.Select(row => row.DataboundItem)
.Cast<Product>();
Back to your question
for reasons (below) I need to exclude a column from DataGridView (not just hide it, truly exclude), ideally preventing it from being generated
If I read your question literally: you don't want to generate the DataGridViewCells that are in columns that are excluded.
This does not influence the Product that each row represents, it only influences the display of these Products. For example: even though each Product has an Id, you might want not to Display this Id.
The most easy thing for this is to use visual studios designer for this. Instead of defining the columns with the DataBinder, just add the columns one by one, and use the properties of each column for the name of the column, the name of the property that it has to show, the format that is used to show the value.
Code will look like this:
DataGridView dataGridView1 = new DataGridView();
// Column to show Product.Id
DataGridViewColumn columnProductId = new DataGridViewColumn();
columnProductId.HeaderText = "ID";
columnProductId.DataPropertyName = nameof(Product.Id);
// Column to show Product.Name
DataGridViewColumn columnProductName = new DataGridViewColumn();
columnProductName.HeaderText = "Name";
columnProductName.DataPropertyName = nameof(Product.Name);
// etc. for all columns that you want to show
Note: in DataPropertyName you store the name of the Property that must be shown in this column. I use the keyword nameof, so if later the name of the property changes, this won't be a problem.
Of course, if you want some special formatting, for example for numbers or dates, you need to set the proper properties as well. This can also be done in visual studio designer.
Once that you have defined your columns, add them to the DataGridView.
To Display the Products is a two-liner:
IDataTableProducts ProductRepository {get;} // initialize in constructor
void ShowProducts()
{
IEnumerable<Product> productsToDisplay = this.ProductRepository.AllProducts;
this.DisplayedProducts = new BindingList<Product>(productsToDisplay.ToList());
}
I stumbled upon an answer: https://stackoverflow.com/a/31282356/5263865
Setting the column.ColumnMapping = MappingType.Hidden does exactly what I needed: the column isn't autogenerated anymore.
DataTable data;
data.Columns[0].ColumnMapping = MappingType.Hidden;
Related
I'm not used to working with DataTables and all its built in functionalities, rather I usually use it for its debug visual display feature.
I'm new to WPF, and although I am experienced with linq/IEnumerable extension methods, I learned that a DataGrid, in order to support editing, the object you assigned to the ItemsSource property must implement a given interface (which I am not sure what it is, for the time being let's say it is either IEditableCollectionView or IBindingList).
Since I am used to collection manipulations via IEnumerable extension methods, here is how I easily manipulate a datatable, filter it and project only the columns I want:
grdSettings.ItemsSource =
_settings
.AsEnumerable()
.Where(row => row["table"].ToString().Equals(e.AddedItems[0].ToString()))
.Select(s => new
{
Setting = s["field"],
Description = s["description"],
CharValue = s["charValue"],
NumMinValue = s["minValue"],
NumMaxValue = s["maxValue"]
});
The problem with this is that the grid will not be editable. If I assign _settings.AsDataView(), the grid is editable. My problem then is I do not want all the columns, and I cannot make (I do not know how) an IEnumerable into a DataView. I read some posts about filtering columns from a datatable/dataview, but the solutions look a bit awkward, not as smooth as I would like...
Isn't there only a small piece I am missing I can "plug" into my solution, instead of having to give up the IEnumerable extension methods filtering... ?
Update: Mike Eason below suggested replacing my anonymous type with a predefined one, stating anonymous types are read only and this is the reason why the grid's collection source does not support editing.
here's the update with a strongly typed object, but it still does not work. Actually it does not make sense to think this alone would make it work, based on the premise stated above, that in order for a grid to allow editing its collection source object must implement a given interface for that.
grdSettings.ItemsSource =
_settings
.AsEnumerable()
.Where(row => row["table"].ToString().Equals(e.AddedItems[0].ToString()))
.Select(s => new GridRecord
{
Setting = s["field"].ToString(),
Description = s["description"].ToString(),
CharValue = s["charValue"].ToString(),
NumMinValue = s["minValue"].ToString(),
NumMaxValue = s["maxValue"].ToString()
});
In the absence of better solutions, I made use of the DataTableProxy nuget package, which will make a DataTable out of a regular IEnumerable<T>, dropped the datatable from my code, and used List<List<string>> instead (because I do not want to create a 2nd instance of a datatable just for the sake of editing the grid) - making a DataTable out of it using the package.
I have my editable grid, but I am still optimistic about simpler solutions :-)
I have multiple DropDown-style ComboBoxes on a form in which the user is able to specify which units are associated with a value (think meters, feet, etc.). To do this, I use an application string collection in Settings.Settings with some default unit types which is bound to the DataSource of the ComboBox as follows:
this.cboUnit.DataSource =
(System.Collections.Specialized.StringCollection)Properties.Settings.Default.Units;
The user is able to successfully pick a unit or specify a new one.
Now, when I save this.cboUnit.Text into some XML profile, I am able to handle both cases. Opening the XML profile and performing this.cboUnit.Text = "NonExistentUnit"; will fail and cause the first item in the collection to be selected instead.
How can I modify the ComboBox to support the kind of behavior I'm asking, without adding the item to the ComboBox or modifying the DataSource itself?
There was a solution suggested in Using ComboBox.Text when combobox is tied to DataSource but I couldn't find official documentation to support this behavior. For that reason I prefer to avoid it.
I could not find any valid way to insert items into a bound DataSource. So instead I ended up doing the following:
BindingSource bindingSource = new BindingSource((System.Collections.Specialized.StringCollection)Properties.Settings.Default.Units, "");
if(!bindingSource.Contains(someSavedValue))
{
bindingSource.Insert(0, someSavedValue));
}
this.cboUnit.DataSource = bindingSource;
This creates a new instance of the binding source that can be modified prior to binding it to a data source. I wrapped this code in a method for which I can pass in a string collection and the stored value (savedUnit in this case) that returns a new BindingSource instance. This is especially useful in my case, as I can now simply write:
this.cboLengthUnit.DataSource = CreateBindingSource(unitsCollection, savedLengthUnit);
this.cboWidthUnit.DataSource = CreateBindingSource(unitsCollection, savedWidthUnit);
this.cboHeightUnit.DataSource = CreateBindingSource(unitsCollection, savedHeightUnit);
I wrote the code above from memory, so it may contain a mistake.
BlaEntities TestContext = new BlaEntities();
IQueryable<TestEntity> Entities = TestContext.TestEntity;
TestDataGridView.DataSource = Entities;
When I assign Entities to TestDataGridView's DataSource directly; I don't have to do anything to reflect my changes to the grid.
TestEntity entity = Entities.First();
entity.Title = "What up!?";
This is more than enough to see the change in the TestDataGridView. One exception I encountered was that if I add another row to the TestContext using TestContext.AddToTestEntity(...) , it doesn't show up in the grid (contrary to deleting it) but I got it working using the BindingSource's Add method.
BindingSource source = new BindingSource{DataSource = Entities};
TestDataGridView.DataSource = source;
source.Add(CreateNewTestEntity());
Now the only obstacle left in my way is this:
If I use filtering - like TestContext.Where(t => t.Active) - use it as DataSource to my grid, then change the first record's Active property to false, how do I refresh/reload the grid to reflect this without creating another instance of BlaEntities?
IQueryable<TestEntity> FilteredEntities =
TestContext.TestEntity.Where(t => t.Active);
TestDataGridView.DataSource = FilteredEntities;
TestEntity temp = FilteredEntities.First();
temp.Active = false;
I see it is not active anymore in grid but since the grid should show only the active records, how can I remove it from the grid without removing it from the source?
When I iterate over FilteredEntities, I can see that the temp isn't there anymore but I still can see and edit it in the grid. So what I need is something forces grid to iterate its DataSource (which is FilteredEntities) as well and populate itself again
I tried calling TestContext's Refresh method and BindingSource's reset methods.
I tried changing TestDataGrid.DataSource to null, then changing it back to FilteredEntities hoping to re-populate the rows, didn't work either.
It works if I save my changes using TestContext.SaveChanges() and use another instance of BlaEntities like TestDataGridView.DataSource = new BlaEntities().TestEntity.Where(t => t.Active) but I need to use my current instance.
So the question is, how can I make the TestGridView to reload its contents using FilteredEntities.
Any advice would be greatly appreciated. Thank you.
since you already have a BindingSource have a look at the ResetBindings method
//edit:
as from the comments below:
i would approach this problem with a factory for "Entities" ... if that factory would hold a ref to the last created IQueryable, it could implement the interface IQueryable itself, by forwarding all interface methods to that created object ... so it could act as a wrapper for your datasource that can replace that datasource by recreating it based on the predicate function and the actual state of all objects
This is a C# Winform question. I have a DataGridView which is bounded to a DataTable. I construct the DataTable myself, which several DataColumn instances. When the DataTable is bound to the DataGridView, by default, every column is sortable by clicking the headers of the DataGridView.
But the sorting behavior is something "by default". It seems that it is sorted by string. This is true even if I put this as my code:
DataColumn dc = new DataColumn("MyObjectColumn", typeof(MyObject));
And MyObject has overriden ToString() and has implemented the IComparable interface. That means even if I have told the DataTable how to sort the special column with the implementation of IComparable interface, DataGridView still doesn't do it the way I expect.
So how can I let DataTable sort data in the way I want?
Thanks for the answers.
I would recommend using the DefaultView of the DataTable. It has some built in sorting features that are little more extendable. The easiest is RowFilter, but I'm not sure if this will be what you're looking for if your data types are overridden as .ToString() at the table level.
EDIT: added code snippet
A custom method like this that maybe even overrides or is called during the sort event of your DataGridView might be able to sort a DataView before the binding actually occurs. But as I understand it, the IComparable never gets called unless you specify it to be called.
protected void SortGrid()
{
System.Data.DataView dv = myDataTable.DefaultView;
myOjbect comparer = new MyObject();
// Comparer specifics go here. Sort order, column/fieldname etc
// or any custom properties used in sorting
dv.Sort(comparer)
dgMyGrid.DataSource = dv
dgMyGrid.DataBind()
}
I had to deal with this today. I've implemented a natural sort (hat tip: Natural Sort Order in C#) and had to sort a DataTable. This is more of a "low fi" method, but it did the trick for the small dataset I'm working with.
I'm creating a key/value value relationship between my sorting column, and the DataRow itself, and popping it into a SortedList constructed with an IComparer.
DataTable myDataTable = {all my data...}
SortedList myDataNaturallySorted = new SortedList(new NaturalComparer());
foreach (DataRow dataRow in myDataTable.AsEnumerable())
myDataNaturallySorted.Add(dataRow["columWithKeyName"].ToString(), dataRow);
Then I moved forward using the sorted list as a data source for my repeater.
I bind some collection to a DataGridView. Collection contains KeyValuePair objects where key is a string and value is an object of my class Field. DataGridView displays two columns - one containing the key and the other one containing the value. The value (Field object) is displayed with its ToString() method. But I would like it to be displayed using its Name property. The problem is the column contains no DisplayMember property.
How can i do it?
Edit: I know I could override ToString() to return the name of the object but I don't want to do that.
DataGridView (in common with most direct list-based bindings) can only bind to immediate properties of the row item. You could perhaps create a facade object for this? i.e. a class that accepts the instance and returns the name as a direct property:
public string Name {
get {return innerObject.Name;}
set {innerObject.Name = value;}
}
// snipped: other properties - Key etc
Alternatively, you could project into a new object? For example, data-bindings work (read-only, at least) with anonymous types pretty well:
grid.DataSource = originalData.Select(x=>
new {x.Key, Name = x.Field.Name}).ToList();
Finally, you can hack around in ComponentModel to flatten the model at runtime, but it really isn't worth it just for this.
You could put the DataGridView into virtual mode (view.VirtualMode = true), and handle the CellValueNeeded (and possibly the CellValuePushed) events to access the "Name" property. This would avoid creating lots of wrapper objects, but does make the code somewhat less elegant.