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 :-)
Related
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;
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
For example, I want a user to create a new fanclub for a given Team.
Obviously the teams have to be loaded into a DropDownList so they are chosen from a pre-set choice group. The list of teams are returned from a database.
As of now I'm doing things like this:
//FindAll() returns IQueryable<Team>
var Teams = teamsRepo.FindAll().AsEnumarable();
myDropDownList.DataTextField = "Name";
myDropDownList.DataValueField = "ID";
myDropDownList.DataSource = Teams;
myDropDownList.DataBind();
Unfortunately when doing things this way I have no strongly typed attributes, so there is a risk of misspelling the ValueField or TextField.
There is also a problem when trying to get the selected value of the DropDownList. Using the following code, everything is saved with ID 1 regardless of what team was chosen.
fansite.IDTeam = myDropDownList.SelectedIndex;
Any suggestions on how to improve?
From your question it seems you're concerned about the hard coded strings for the databinding in your code. The same thing used to drive me nuts too.
There is a good way of using lambda methods and expression trees that will allow you to get rid of the hard coded strings and get the property names in a strongly typed way:
You can use this class and extension method...
public class Nameof<T>
{
public static string Property<TProp>(Expression<Func<T, TProp>> expression)
{
var body = expression.Body as MemberExpression;
if(body == null)
throw new ArgumentException("'expression' should be a member expression");
return body.Member.Name;
}
}
...and this will allow you to do the following:
//instead of myDropDownList.DataTextField = "Name";
myDropDownList.DataTextField = Nameof<Team>.Property(t => t.Name);
//instead of myDropDownList.DataValueField = "ID";
myDropDownList.DataValueField = Nameof<Team>.Property(t => t.ID);
That's what I like to see; not a hard coded string literal in sight! :-)
This is exactly how .NET databinding works, you have to type in the property names.
You could always add a converter to a combobox item and then call add (ms-doc)
As far as getting the value of the selected item try this:
fansite.IDTeam = myDropDownList.SelectedValue;
(This was written before the ASP.Net clarification, I have answered from a winforms persepective).
I'm not sure what your Teams IDs are like, but if they are not sequential, zero based and with no gaps, using myDropDownList.SelectedIndex; will give you the wrong piece of data.
I think you last line should read:
fansite.IDTeam = myDropDownList.SelectedValue;
On the data type to use, I usually use a Dictionary<int, string> for dropdown lists, and have a few helper extension methods to make population a little nicer.
You should consider using DataSources like ObjectDataSource or LinqDataSource to DataBind your controls.
It will save you time and is less error prone because you will configure your settings through a wizard instead of defining them in your code-behind.
Is it possible to use a primitive-typed Collection, such as List<long>, as the DataSource for a DataGridView?
After a half-hearted attempt to make it work, I gave in and created a simple struct so I could give DataGridViewColumn a PropertyName. But now I have to box my values when I deal with the UI, unbox them for the rest of my app (thus negating the benefits of data binding), and implement IComparable and IEquatable in my struct to support List sorting and searching (a simple enough task, the existence of the struct itself is cluttersome enough).
It seems like a while lot of overhead and clutter just to give a list of values to a UI widget. Surely there is an easier way...
If you have a list of numbers that you want to show to the user consider using a ComboBox. This way you can set the DataSource to the List<long>.
However, if you really need the DataGridView you can change your List<long> to List<Long?> and you can now bind a grid view column to the Value property of the bounded list. This approach saves you the custom struct, but it's still a compromise solution.
#João's solution is nice if you have control over your code.
If you don't control the list coming in, you could wrap it in a linq expression in order to get a named entity to bind to.
List<int> list = new List<int> {1, 1, 1};
var q = from item in list select new { bindingname = item };
dataGridView1.DataSource = q.ToList();
I'm trying to create a DataGridTableStyle object so that I can control the column widths of a DataGrid. I've created a BindingSource object bound to a List. Actually it's bound to an anonymous type list created though Linq in the following manner (variable names changed for clarity of what I'm doing):
List<myType> myList = new List<myType>(someCapacity);
.
...populate the list with query from database...
.
var query = from i in myList
select new
{
i.FieldA,
i.FieldB,
i.FieldC
};
myBindingSource.DataSource = query;
myDataGrid.DataSource = myBindingSource;
Then I create a DataGridTableStyle object and add it to the datagrid. However, it never applies my table style properties I set up because I can't seem set the proper myDataGridTableStyle.MappingName property.
I've searched Google for about 1/2 an hour and keep seeing links to the same question throughout a bunch of different forums (literally the same text, like someone just copied and pasted the question... I hate that...). Anyway, none of the suggestions work, just like the guy says on all the other sites.
So does anybody here know what I need to set the MappingName property to in order to have my TableStyle actually work properly? Where can I grab the name from? (It can't be blank... that only works with a BindingSource that is bound to a DataTable or SqlCeResultSet etc.).
I'm thinking it could be an issue with me using Linq to create an anonymous, more specialized version of the objects with only the fields I need. Should I just try to bind the BindingSource directly to the List object? Or maybe even bind the DataGrid directly to the List object and skip the binding source altogether.
Thanks
PS - C#, Compact Framework v3.5
UPDATE:
I've posted an answer below that solved my problem. Whether or not it's the best approach, it did work. Worth a peek if you're having the same issue I had.
I've found the way to make this work. I'll break it out into sections...
List<myType> myList = new List<myType>(someCapacity);
.
...populate the list with query from database...
.
DataGridTableStyle myDataGridTableStyle = new DatGridtTableStyle();
DataGridTextBoxColumn colA = new DataGridTextBoxColumn();
DataGridTextBoxColumn colB = new DataGridTextBoxColumn();
DataGridTextBoxColumn colC = new DataGridTextBoxColumn();
colA.MappingName = "FieldA";
colA.HeaderText = "Field A";
colA.Width = 50; // or whatever;
colB.MappingName = "FieldB";
.
... etc. (lather, rinse, repeat for each column I want)
.
myDataGridTableStyle.GridColumnStyles.Add(colA);
myDataGridTableStyle.GridColumnStyles.Add(colB);
myDataGridTableStyle.GridColumnStyles.Add(colC);
var query = from i in myList
select new
{
i.FieldA,
i.FieldB,
i.FieldC
};
myBindingSource.DataSource = query.ToList(); // Thanks Marc Gravell
// wasn't sure what else to pass in here, but null worked.
myDataGridTableStyle.MappingName = myBindingSource.GetListName(null);
myDataGrid.TableStyles.Clear(); // Recommended on MSDN in the code examples.
myDataGrid.TablesStyles.Add(myDataGridTableStyle);
myDataGrid.DataSource = myBindingSource;
So basically, the DataGridTableStyle.MappingName needs to know what type of object it is mapping to. Since my object is an anonymous type (created with Linq), I don't know what it is until runtime. After I bind the list of the anonymous type to the binding source, I can use BindingSource.GetListName(null) to get the string representation of the anonymous type.
One thing to note. If I just bound the myList (which is type "myType") directly to the binding source, I could have just used the string "myType" as the value for DataGridTableStyle.MappingName.
Hopefully this is useful to other people!
Just to add to the collection of answers already on this page....
I was just frustrated with this same issue trying to develop my fist application using windows forms and compact framework (For Windows Mobile 6.5).
What I found out, through Marc Gravell's comment above is that indeed is possible to get the run time MappingName inspecting the properties of the DataGrid. Doing this I found out that when binding my List<MyType> directly to the DataSource property of the DataGrid, the DataGrid was actually looking for a DataGridTableStyle with the MappingName of
"List`1"
instead of any combination of List<MyType> or MyType...
So... by putting "List`1" in the Mapping name on the DataGridTableStyle Collection Editor (at design time), I was able to customize the columns and other properties without having to create them all at run time.
I just hope this adds some more to the answers already provided. Thank you all for providing me with the guidelines.
The query returns IEnumerable<T> for some T, but most binding sources (except ASP.NET) require IList (such as any IList<T> implementation) - try adding .ToList() - i.e.
myBindingSource.DataSource = query.ToList();
A BindingList<T> might work even better (if it is supported in CF 3.5) since it has better support for some of the common binding scenarios; if you need this (and assuming BindingList<T> exists on CF 3.5), you can add an extension method:
static BindingList<T> ToBindingList<T>(this IEnumerable<T> data)
{
return new BindingList<T>(new List<T>(data));
}
then call:
myBindingSource.DataSource = query.ToBindingList();
For completeness, an alternative to an IList is IListSource (or even Type for purely-metadata scenarios), which is why DataSource is commonly typed as object; if it wasn't for this issue, the compiler probably would have been able to tell you the problem (i.e. if DataSource was defined as IList).
I followed this answer and found that the MappingName always came out to be the underlying class name (myType in the example).
So it seems that putting the collection it through the BindingSource solves the problem anyway and that there is then no need for BindingSource.GetListName(null).
Also I found no need to ToList() the query as the BindingSource will also do this for you.
Many thanks to Jason Down for putting me on the right track.
I was facing same problem for setting column width.
After lot of R & D, i changed code as below and its working fine.
Code:
DataGridTableStyle tableStyle = new DataGridTableStyle();
tableStyle.MappingName = dgCustom.DataSource.GetType().Name;
where dgCustom is DataGrid ID in dgCustom.DataSource.GetType().Name which is working perfectly.