I am creating Windows Application which has a Grid.
Grid's data source is a an Object of a Class.
Grid has two bands.
As shown in the below image on Band1, there is a Column named as Templates.
It has values from 1 to 10.
The requirement is that based on the selected values in the Template Field, Band2 must have no of rows.
e.g. if User selects 2 in the Template Field, Band2 must have two rows.
As its a run time process, Grid must be refreshed on the Run time.
In the below case if the value is changed from 2 to 3 in the template field, the open Band2 must be refreshed to show 3 rows.
I wrote some code but it was not able to refresh the grid on the run time.
private void grdDataMapping_AfterCellUpdate(object sender, CellEventArgs e)
{
if (e.Cell.Column.Key.Equals("TemplateName"))
{
ValueList paramName = new ValueList();
string templateName = e.Cell.Text;
List<TemplateMapping> tempMappings = new List<TemplateMapping>();
if (_dictTemplateNames.ContainsKey(templateName))
{
for (int i = 0; i < templateName.Value; i++)
tempMappings.Add(new TemplateMapping());
mappingDetails.ListTemplateMapping = tempMappings;
}
grdDataMapping.Refresh();
}
What am I missing here?
You could implement the interface INotifyPropertyChanged and some additional properties you may notify of. This way the ViewModel notifies the View (or vice versa) about any changes that have been made. Thus, you can easily control the content of all 3 DataGrids.
See INotifyPropertyChanged for more on this. There are plenty of articles around who help you archiving this.
Related
I have an input enabled DataGridView bound to a BindingSource which has a SortableBindingList as DataSource.
There is no direct connection to the database. The list is read once from the database beforehand. After the user is done with editing he can choose either to save the changed data to the database or not.
The list has 2 fields:
"Type" (enum)
"Path" (string)
As I want to use a ComboBox for the user to select the "Type" I add an additional column "TypeCbx" which is bound to the enum values. Initially, all "Type" values from the list are copied to the column "TypeCbx" and for changes, the value of "TypeCbx" is copied back to "Type" in the event ...CellEndEdit().
Furthermore, I have 2 button columns included:
"Browse" button: opens a FolderDialog for the user in order to adjust the actual row and set the column "Path" and (if it is a new row/entry) a default value to "Type"
"Remove" button: removes the row/entry from the DGV
This worked as long as I used an unbound DataTable in which I had the following code for the button "Browse":
private void dgvPaths_OpenFolderClick(DataGridView sender, DataGridViewCellEventArgs e) {
string newSelectedPath = Helper.FileBrowserDialog("Select folder", LastSelectedPath);
if (newSelectedPath != null) {
LastSelectedPath = Helper.CleanPath(newSelectedPath);
if (dgvPaths.Rows[e.RowIndex].IsNewRow) {
// --- variante old: unbound datatable ----------------------------------------
DataGridViewRow row = (DataGridViewRow)dgvPaths.Rows[0].Clone();
row.Cells[dgvPaths.Columns["Path"].Index].Value = LastSelectedPath;
row.Cells[dgvPaths.Columns["Type"].Index].Value = LibraryPathType.Movies;
row.Cells[dgvPaths.Columns["TypeCbx"].Index].Value = LibraryPathType.Movies;
row.Cells[dgvPaths.Columns["TypeCbx"].Index].ReadOnly = false;
dgvPaths.Rows.Add(row);
dgvPaths_CellValueChanged(sender, e);
} else if (dgvPaths.Rows[e.RowIndex].Cells["Path"].Value == null || LastSelectedPath != dgvPaths.Rows[e.RowIndex].Cells["Path"].Value.ToString()) {
dgvPaths.Rows[e.RowIndex].Cells["Path"].Value = LastSelectedPath;
dgvPaths.Rows[e.RowIndex].Cells["TypeCbx"].ReadOnly = false;
dgvPaths_CellValueChanged(sender, e);
}
}
}
Now, with the bound DataSource, the line dgvPaths.Rows.Add(row); no longer works. So I adjusted the code as follows:
// --- variante new 1: bound list, working on dgv -----------------------------
dgvPaths.Rows[e.RowIndex].Cells["Path"].Value = LastSelectedPath;
dgvPaths.Rows[e.RowIndex].Cells["Type"].Value = LibraryPathType.Movies;
dgvPaths.Rows[e.RowIndex].Cells["TypeCbx"].Value = LibraryPathType.Movies;
dgvPaths.Rows[e.RowIndex].Cells["TypeCbx"].ReadOnly = false;
dgvPaths_CellValueChanged(sender, e);
Issue 1:
Now, the data is written into the row of the DataGridView but the DataGridView does not interpret it as an input and therefore it is not really added - it still is a "new Row" waiting for input. I need to manually go into the Path Column of the row and press a key in order that the DataGridView accepts it as a valid entry and shows a new "new Row" line.
=> How can I inform the DataGridView that programmatically entered data should be handled like a user input?
Issue 2:
Furthermore, when I manually enter an entry in the DataGridView and click in the "Path" column between the added line and the new "new Row" line, a first chance exception is thrown.
=> What is the reason for the first chance exception?
Then I've read that you should not manipulate the DataGridView but instead the BindingSouce or BindingSource.DataSource, which I tried with by changing the code to this:
// --- variante new 2: bound list, working on datasource ----------------------
Library.Current.AddDirtyPath(LibraryPathType.Movies, LastSelectedPath);
Issue 3:
Hereby, I also get a first chance exception when this entry is added to the source list.
=> What is the reason for the first chance exception?
What is the correct approach here?
=> Do I need to manipulate the rows of the DataGridView or the entries of the BindingSource or the entries of the BindingSource.DataSource?
Issue 4:
The bound DataGridView threw another exception when loading the DataSource and there is no enum value for "0" (I guess for the "new Row" line). Therefore, I needed to add a dummy enum value which is set to "0" to my enum value list which I need to skip again for the actual ComboBox selection values. It works but it messes up the code.
=> Is it possible to avoid this dummy value, at all?
--- U P D A T E ---
After reading Caius recommendation, I have decided to update this question as I was able to follow the SBL approach and reduce the issues.
The correct approach here is to use a main "storage" SBL for all working data (which is initially filled by reading the DB) and create a filtered SBL out of it which is used as DS for the DGV where the user can work on and use sort and filter methods. By adding/updating/removing data, you have to ensure that all adjustments stay synchronous in the "storage" SBL. Then, when you want to update the DB, you use the 'storage' SBL.
Thereby only 1 issue is left: if you want to add a new line with a button column function inside the DGV itself. It fails as the state of the actual new line is changed while you are calling the add function from inside the DGV. There are 2 ways to "fix" this:
a) You need to completely(!) Clear() the SBL that is used as DS for the DGV and add the new line afterwards (according with all other existing ones from the "storage" list) back to this SBL. Thereby the state of the new line is also changed but as it is completely removed the state is also cleared. After this you need to Refresh() the DGV. Hereby, you will lose focus of the actual cell.
b) You use a hidden button outside of the DGV by btnAdd.PerformClick() which calls the add function in which you do not need to Clear() the SBL that is bound to the DGV. This seems strange but it works (whyever) and you keep the focus on the "new line" row (not on the added one).
All other functions like updating and removing an existing DGV line can be called by additional button columns within the DGV itself without issues.
The enum issue is not an issue as it is common to use a dummy zero value for empty entries. If I find out, how to get rid of the dummy value, I will update this question accordingly.
In order to fill the combobox column without a split between display and value columns you only need to ensure that the DataPropertyName of the combobox column is set like the DB column name.
--- U P D A T E: 2 ---
I have created a detailed video (tutorial) about my approach:
https://www.youtube.com/watch?v=W_afaNf7nz8
From 1:31:20 I show the difference between adding a new line by an external and an internal button and also the strange behaviour of using the method directly (=> error) or triggering an external button which uses the same method (=> no error).
What is the correct approach here?
You have a datagridview
You bind its DataSource to a datatable
You have a datagridviewcombobox
You bind it's DataSource to a completely different datatable
You tell the combo which columns, from its own datatable, are to be used for display, value and which column in the table that the grid is bound to, shall be updated/used for deciding which value to Show
var gdt = new DataTable();
gdt.Columns.Add("FileType", typeof(FileType));//enum
gdt.Columns.Add("Path");
gdt.Rows.Add(FileType.Text, "c:\my.txt");
var fdt = new DataTable();
fdt.Columns.Add("Val", typeof(FileType));//enum
fdt.Columns.Add("Disp");
foreach(FileType t in Enum.GetValues<FileType>())
{
fdt.Rows.Add(t, t.ToString());
}
//now wire it up
datagridviewWhatever.DataSource = gdt; //makes columns
var c = new DataGridViewComboBoxColumn();
c.DisplayMember = "Disp"; //name of column in fdt to use for show
c.ValueMember = "Val"; //name of column in fdt to use for value during lookup/set operations
c.DataPropertyName = "FileType"; //name of column in gdt that this combo shall show/set
c.DataSource = fdt; //set DataSource last (performance reasons)
Combo will now, for each row, get the value in gdt.FileType (eg FileType.Text), lookup the value in its own table fdt.Val, use the related value in fdt.Disp (eg "Text") to show in the list. If the user chooses a new list item (eg "Excel") it goes the other way, get the relevant Val (eg FileType.Excel) chosen and push it into gdt.FileType
I've never actually done it with enum typed values; can't see why it wouldn't work but if it gives trouble it might be simpler to switch to using ints instead of FileTypes - make all your typeof() calls typeof(int) and cast the FileType t to int in the foreach when adding it to the row collection.
Finally, when you're down with this as a concept I recommend throwing it all away and doing it using the visual designer - it will make a much better, nicer, easier to use job of it:
add a DataSet type file to your project
open it
if there is a database somewhere backing all this, right click on the design surface and choose Add TableAdapter, enter the connection string, choose a query, that returns rows, put SELECT * FROM MovieFiles WHERE ID = #id or whatever, call it FillById/GetDataById, finish
if there isn't a database backing all this just right click the surface, add a datatable called MovieFiles, right click it and add columns like FileType (set type to int in property grid), Path etc
right click a blank space and add another datatable called FileTypes, put a string column called Disp and an int column called Val (sounding familiar?)
save and switch to Forms designer. Open Data Sources tool panel (View menu.. other windows submenu)
Drag the MovieFiles node out of data sources and into the form. A datagridview appears as well as a bunch of stuff in the bottom tray. The grid is already correctly binded to the DataSet that contains the MovieFiles table. You can see all the code VS is writing for you in the Forms.Designer. It looks a lot like the code I put manually above
Edit the columns of the datagridview, change the file type column to be a combobox type, set the DataSource to the FileTypes table, DisplayMember to Disp, ValueMemberto Val. Check the DataPropertyName is still FileType in movies - this should also be familiar as it's the visual version of the code above)
the only thing left to do is put some code in the constructor that fills the FileTypes table from the enum (or to be honest, just make a table in the DB called FileTypes, have an int and a string column, add a tableadapter, select them.. that way you don't have to recompile the whole program when you add a file type)
Why do I advocate using these visually designed datatables rather than code ones? Because they're so much nicer in every single way. They have logical methods and named properties for columns, they just work with LINQ and they behave like .net classes that are collections of POCOs not 2D arrays of object that are indexed by string and need casting to make them useful. DataTable are awful to work with in comparison. They wrap around basic DataTable and do expose them but follow a rule that if you've exposed it, you've probably gone wrong
//no; using .Tables puts you back in "world of pain"
var p = (string)datasetX.Tables["MovieTypes"].Rows.Cast<DataRow>().First()["Path"];
//yes - using named property for MovieTypes is good, LINQ works, Path is a string property etc
var p = datasetX.MovieFiles.First().Path;
I have a DataGridView which is filled by a BindingSource. Everytime that the user modifys a row and he/she moves to another row (Everytime the row modified lose the focus) it triggers the event RowValidated.
In this event, the elements of the DataGridView.CurrentRow are modified in the SQLServer with a stored procedure.
When the BindingSource have a filter active this approach doesn't. Because if is modified the cell of the column which is filtered, the row dissapers (Because of the filter) and my method catch the DataGridView.CurrentRow as the next one.
I cannot use the method TableAdapter.Update() because the DataSet is a join of multiples tables and it doesn't autogenerates.
Is there any event that is triggerred before the effects of the filter?
I would accept as answer any different approach.
Edit: When a cell is modified, is saved in a List the ID value of that row. The code in RowValidated is encapsulated in
if(ListOfIds.Contains(DataGridView.CurrentRow.Cells["ID"])){StoredProcedure();}
It is seldom a good idea to read and write the values of the cells yourself. Using DataBinding is usually an easier method.
It seems that you already use a BindingSource, but for some reason you read the changed row from the displayed data, not from the original BindingSource.
I'm not sure if it is wise to update your DataBase as soon as the operator moves to a new row. What if he doesn't know the value to fill in, and wants to scroll down to check another row? Consider to change the interface such that the operator can indicate that he finished editing all data. For instance a button. But this is a bit outside your question.
If you put your data in a BindingList, and assign this data to the DataSource of your DataGridView, then every change of the data in the DataGridView is automatically updated in the BindingList. Use event BindingList.ListChanged to get notified, or use the above mentioned button before processing the changed data.
BindingList<Customer> DisplayedCustomers {get; } = new BindingList<Customer>();
public MyForm()
{
this.InitializeComponents();
this.DataGridView1.DataSource = this.DisplayedCustomers();
this.BindingListView.ListChanged += BindingListChanged;
}
An empty DataGridView is shown. To fill it with Customers:
private InitializeDataGridView()
{
IEnumerable<Customer> customers = this.QueryCustomers();
foreach (var customer in Customers)
this.Customers.Add(customer);
}
This will automatically show all Customers in your DataGridView. Alas you will get the event once per customer. If you don't want that, consider to create a new BindingList and assign that to the DataSource of the DataGridView. Don't forget to unsubscribe the old BindingList and to subscribe to the event of the new BindingList
private void BindingListChanged(object sender, ListChangedEventArgs e)
{
BindingList<Customer> bindingList = (BindingList<Customer>)sender;
// use e.ListChangedType to detect what has changed:
// item added / removed / moved to a new location / value changed?
switch (e.ListChangedType)
{
case ListChangedType.ItemChanged:
var changedCustomer = bindingList[e.NewIndex];
ProcessChangedCustomer(changeCustomer);
break;
case ...
}
}
The advantage of using the BindingList instead of fiddling with the values of the cells of the DataGridView, is that you decouple the data from how this data is communicated with the operator. This makes it easier to reuse (show it in a ComboBox instead of a DataGridView? Go ahead, changes are minimal), easier to unit test: BindingList also works if there is no form; and therefore easier to maintain and change.
One final advice: to facilitate filtering on properties (show only the Customers that live in New York), and to make sorting easy, consider to use nuget package Equin.ApplicationFramework.BindingListView
List<Customer> customers = GetCustomers();
BindingListView<Customer> view = new BindingListView<Customer>(customers);
dataGridView1.DataSource = view;
And Bingo: free sorting by mouse click the column header. It even remembers whether to sort ascending or descending and it shows the sorting glyph (the arrow that indicates the sorting direction).
Filtering:
void FilerCustomersByCity(string city)
{
view.ApplyFilter(delegate(Customer) customer => customer.City == city);
}
I have a form that a user can fill out to add a new entry to the DB or update an existing one. I need a view that can display the objects number and description and can be selected by a user to prepopulate the form with that objects data for updating.
I tried a data grid view and I got my objects displaying but it shows ALL objects attributes. I only want two of them to display. Also data grid views seem to be selectable by cell not row.
This is how I bound my data. Is there a way to make it selectable by row and give me back the object? Or only display some attributes?
var source = new BindingSource();
List<RollingStock> rollingStockList = RollingStockDAO.GetRollingStocks();
source.DataSource = rollingStockList;
dgvInventoryList.DataSource = source;
I'm still learning to code and Im not super familiar with C# yet. Any help is appreciated!
I tried a data grid view and I got my objects displaying but it shows
ALL objects attributes. I only want two of them to display. Also data
grid views seem to be selectable by cell not row.
those are both properties you need to specify.
to stop the other columns from being created use:
dgvInventoryList.AutoGenerateColumns = false;
to make the entire row get selected instead of just the cell use:
dgvInventoryList.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
im sure they are also in the property grid somewhere.
Is there a way ... give me back the object?
to get the object bound to the row use:
DataGridViewRow.DataBoundItem
then cast it to your object:
MyObject rowObject = dgvInventoryList.Rows[0].DataBoundItem as MyObject;
to get the object when the row is clicked, you can handle the Cell.Click event:
private void dgvInventoryList_CellClick(object sender, DataGridViewCellEventArgs e)
{
MyObject rowObject = dgvInventoryList.Rows[e.RowIndex].DataBoundItem as MyObject;
}
there is also a HeaderCell.Click event and a few others. you should be able to find one to suit your needs.
So I am using the DataListView variant from BrightIdeasSoftware for my c# project.
I use buttons to change the view of the DataListView. When a button is pressed the following is executed:
olv.DataSource = null;
olv.AllColumns = colList;
olv.RebuildColumns();
//Fill OLV with data
olv.DataSource = dt;
The first button creates 1 column and changes the view to View.Tile;.
The second button creates 4 columns and changes the view to View.Details;.
The new columns are shown immediately but it takes about a second for the data to show up in the list. It takes even longer when I rebuild a larger amount of columns.
When I run my application it builds the view with 4 columns in View.Details instantly.
Only when I switch from the first button view to the second button view it hangs for a moment.
In debug mode I noticed that RebuildColumns() is the one that hangs.
But if I leave olv.DataSource = dt; out, code after RebuildColumns() is executed immediatly.
Can someone explain to me why this is happening?
Thanks
First of all it is not clear to me if the lists that you are switching between use the same DataTable (I assume that's the type of your dt object). If source is identical then you needn't add and remove columns, you can set OLVColumn.IsVisible. That's faster.
Second, setting ObjectListView.DataSource = null won't remove items from the list (you need ObjectListView.ClearObjects for that), but what that will do is invalidate the internal DataSourceAdapter.CurrencyManager which will block any item updates until ObjectListView.DataSource is specifically set again. Setting the data source will add items to your list (calls ObjectListView.BuildList) which might be expensive if your source is large.
In conclusion:
If you just switch between lists with shared datasource then:
foreach (var column in this.dataListView.AllColumns)
column.IsVisible = true;
// call this only when tampering with columns
this.dataListView.RebuildColumns();
// if you need to add/remove items, same philosophy, partial instead of
// complete update use filtering instead of DataSource reset
If lists do not have a common datasource and therefore columns totally differ, then it is a price you have to pay, rebuilding columns and items, but you could escape with a TabControl for instance. You could switch between lists without having to reset DataSource all the time. That should be a one time experience.
This will take some explaining.
I'm writing a tool in WPF / C# to dynamically generate, in the view, a visual graph of the data in the view-model. The top-most parent is a grid, and each horizontal row of data is a canvas (inside a border). The canvas holds all the other UI elements (like TextBlocks).
I have a class to hold each row of UI elements in the view.xaml.cs, defined like this:
class ReportRow
{
public Border Divider;
public Canvas Row;
public TextBlock Title;
public List<TextBlock> Phases = new List<TextBlock>();
}
Then I define the entire graph as a List of these Rows:
List<ReportRow> reportRows = new List<ReportRow>();
In the viewmodel.cs, I have the data listed as an ObservableCollection so I can data bind to it and access the data from the view:
public ObservableCollection<SDDeliverable> Deliverables
{
get
{
return this.deliverables;
}
private set
{
this.deliverables = value;
this.RaisePropertyChanged(() => this.Deliverables);
}
}
Back in the view, I loop through the ObservableCollection, creating the rows and assigning the data to the elements (shown without all the styling and positioning, for brevity):
reportRows.Add(new ReportRow());
reportRows[i].Divider = new Border();
ProjectDisplay.Children.Add(reportRows[i].Divider);
reportRows[i].Row = new Canvas();
reportRows[i].Divider.Child = reportRows[i].Row;
reportRows[i].Title = new TextBlock();
reportRows[i].Title.SetBinding(TextBlock.TextProperty, new Binding(string.Format("Deliverables[{0}].DeliverableTitle", i)));
reportRows[i].Row.Children.Add(reportRows[i].Title);
Now, my original problem was that, because I'm binding each individual member of the collection (rather than binding the whole collection to one UI element, like a ListView), the view has no idea how long the collection is, which means I can't use a "foreach" or a loop counter. It worked fine with an arbitrary number of rows, but I didn't want to have to guess.
What I did was add a new label to the UI, bound to the length of the collection, and disguised it as a bit of helpful info:
<Label x:Name="DeliverableCountLabel" Content="{Binding Path=DeliverableCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
this.DeliverableCount = Deliverables.Count;
Once I got that number into a view control, I was able to use it as my loop counter:
int totalItems;
if (int.TryParse(DeliverableCountLabel.Content.ToString(), out totalItems))
{
for (int i = 0; i < totalItems; i += 1)
{
reportRows.Add(new ReportRow());
This is a hacky fix, but I was alright with using it once.
The new problem is that once wasn't enough. As I add more data to the model, I'm hitting the same problem. Each Deliverable has a list of Phases and each Phase has a list of Tasks. I don't want to clutter up the UI with number labels all over the place.
I feel like there should be a way to use a data binding without having it go through the xaml or a visual control element. I just want to bind a variable in the view to a variable in the view-model so I can look at certain bits of info that I don't necessarily want to show the user.
I started messing with doing it this way:
Binding testBinding = new Binding("DeliverableCount");
However, it's the next step that's confounding me. Everything I've tried past that point has been incorrect somehow.
// returns the binding object itself, not the bound value
testBinding.ToString();
// error (not a real thing you can do, apparently)
string testString;
testString.SetBinding (testBinding);
How do I send a value from view-model to view without having to display it on-screen somewhere? Am I going about this the wrong way? Is this even possible?
A last-ditch idea I have is to create one dummy label and either make it invisible somehow or hide it behind another element. Then I could write a function to update the data binding on this one specific label any time I needed to access something in the view-model that's not shown on-screen. However, this really feels like a hack of a hack and I'd rather not go down that road unless it's really the best (or only) option.
This is how I ended up solving this. It's hacky, but it works.
I created a label in the xaml and set it's visibility to hidden. Then I just call one of these functions:
public string TempStringBind(string bind)
{
DummyLabel.SetBinding(Label.ContentProperty, new Binding(bind));
return DummyLabel.Content.ToString();
}
public int TempIntBind(string bind)
{
DummyLabel.SetBinding(Label.ContentProperty, new Binding(bind));
int newInt;
if (DummyLabel.Content != null && int.TryParse(DummyLabel.Content.ToString(), out newInt))
{
return newInt;
}
else
{
return -1;
}
}
This will take any variable from the view-model that can be bound to, bind it to an invisible label, grab that value from the label, and return it to me in the view in a useable form. While it's still going through something in the view xaml, the benefit is that the user doesn't have to see a bunch of extra controls or data they don't care about just to find out how many rows or columns I need to make for lists.
The ItemsSource method would be a lot cleaner, but that only works if I'm sticking the data in an existing control, like a ListView or a ComboBox, which aren't good for making visual charts and graphs with exact positioning.
I'm not sure what you guys meant by it not being MVVM. I've got the M, the V, and the VM all in there. :P