How to handle calculated fields in otherwise bound forms? - c#

This concerns an approval field on a form. In the database, it's a bool field, an int field containing a FK to approvers, and a date-time field that, together, indicate whether something was approved and if so, who approved it and when. On the form, this has to translate into something like "Approved by John Smith on 01/02/03 04:05."
I handle this with a navigation bar. When the binding source position changes, the event is trapped and the code formats the calculated fields, like this (what the code does it not that important).
private void ctlNavBar1_displayCurrent(object sender, EventArgs e)
{
var drv = talsBindingSource.Current as DataRowView;
if (drv != null)
{
ctlBoundCheckButton1.lblText = $"Submitted {drv.Row.Field<DateTime>("SubmitDate").ToString("MM/dd/yy hh:mm tt")}";
ctlBoundCheckButton1.setControls(true);
if (drv.Row.Field<bool>("Approved"))
{
var sup = talsSupervisorsBindingSource.Current as DataRowView;
ctlBoundCheckButton2.lblText = $"Approved by {sup.Row.Field<string>("FullName")} on {drv.Row.Field<DateTime>("ApproveDate").ToString("MM/dd/yy")}";
ctlBoundCheckButton2.setControls(true);
}
}
else
{
using (DialogCenteringService centeringService = new DialogCenteringService(this))
{
MessageBox.Show("No TALs to Approve", "Confirm", MessageBoxButtons.OK);
}
Close();
}
}
The problem is that
public TALsApprove()
{
InitializeComponent();
talsTableAdapter.FillForApproval(timeTrackDataSet.TALs, User.ID);
usersTableAdapter.FillBySupervisor(timeTrackDataSet.Users, User.ID);
timeSlipsTableAdapter.FillBySupervisor(timeTrackDataSet.TimeSlips, User.ID);
ctlNavBar1.displayCurrent += ctlNavBar1_displayCurrent;
ctlNavBar1.bindingSource = talsBindingSource;
// this assignment doesn't fire Position Changed (or anything else, as far as I can tell)
}
the binding source event PositionChanged does not fire when the binding source is first assigned. I've worked around that by using the form Shown event, like this.
private void TALsApprove_Shown(object sender, EventArgs e)
{
ctlNavBar1_displayCurrent(null, new EventArgs());
}
So my questions are:
1.- Does calling event handlers directly like this mess with any of .NET's internals? (e.g. memory leaks, stack problems, etc.)
2.- Is there a less kludgy way of handling the calculation of fields when the binding source is first initialized, as well as when the contents of the current record change? I experimented with the binding source events CurrentChanged and CurrentItemChanged, but they seem to over-fire, firing even when no actual field value had changed.

There are a couple of ways I might think to tidy this up:
1 ) Use a calculated column
Assumptions:
You have a strongly typed DataSet with two tables like Applications and Users
You have the following columns in Applications: Approved(bool), ApproveDate(datetime), ApprovedByUserId(int)
You have a single datarelation between Applications and Users that maps Applications.ApprovedByUserId (many) to User.UserId (one), and UserId is also an int
Process:
In your dataset click in your Applications table, and add a string column
Set its Expression property to something like: IIF([Approved] = False,'Not approved','Approved by' + Parent.Username + ' on ' + [ApproveDate])
You have some process alreadythat fills good data into the table:
When you run the app it becomes the datatable's problem to build the narrative:
Let's edit another detail in at runtime:
When you finish the edit and move off the row it will be committed to the table and the narrative updates automatically
You can read more about what syntax you can use in DataColumn.Expression
If you don't have a bool approved you could add one or use some other test like IIF(ApprovedByUserId IS NULL,'Not Appproved,'App...'). If yo uhave multiple datarelations coming off Applications you specify the name of the relation after Parent like Parent(App_User).UserName` assuming the datarelation is called App_User
Bind different things on the UI
Noone ever said you only had to bind Text. If you had a "Approved" bool column in your dataset, you could have several labels in a row:
--label1----- --label2------ --label3-- --label4--
"Approved by" BindParentName " on " BindDate
You can bind every one of their Visible properties to the Approved bool so the labels disappear if the user navs to an unapproved row.
The easiest way to get the parent user name into the Applications data table (because all these labels are bound to a indingsource that sits on the Applications table, right?) is to use an Expression on a new column like above, but simpler (just Parent.UserName or Parent(App_User).UserName`) to import the user name into the Applications datatable.
There are other ways, involving multiple binding sources that bind to datarelations.. We can even jiggle a combo box into doing it - the combo has a DataSource of the users table, but a DataMember of "ApprovedByUserId" from the applications table; it will perform 2 way lookup of the ApprovedByUserId <--> UserId equivalence

Related

Unable to give a default value to a filtered table

I've just signed up today. I've been writing and maintaining my company's Delphi ERP program for years. Last year my boss wanted me to rewrite the whole ERP using C#. It took my many month to learn and build my basic C# code. Everything went well. Delphi skills help C# coding. Anyway it's still Windows program. But when it came to master-detail structure, I got stuck in a swamp.
It's a piece of cake using Delphi to write a master-detail program. The only thing I need is filter. Unlike C#, A Delphi DataGrid always has the same number of rows as the corresponding table does. Because I used filters to achieve master-detail in Delphi, I did the same (I didn't use ralation) with C#, and the result was a mess.
What's wrong with the way I implement master-detail ? Do I have to use relation ? I am willing to use relation because sometimes I want to access all the rows in a table. Or else, could any one give me a good example of master-detail ?
To save your time, I just outline things.
master : DataGridView1 <- BindingSource1 <- Table1
detail : DataGridView2 <- BindingSource2 <- Table2
KeyColumn : RelateNo
filter inside dataGridView1_SelectionChanged():
bindingSource2.Filter = string.Format("RelateNo = '{0}'", ds.Tables["Table1"].Rows[bindingSource1.Position]["RelateNo"].ToString());
default values neccessary for filter :
ds.Tables["Table1"].RowChanging -= new DataRowChangeEventHandler(DetailRow_Changing);
ds.Tables["Table2"].RowChanging += new DataRowChangeEventHandler(DetailRow_Changing);
private void DetailRow_Changing(object sender, DataRowChangeEventArgs e)
{
if (e.Action == DataRowAction.Add)
{
e.Row.Table.Columns["RelateNo"].DefaultValue = ds.Tables["Table1"].Rows[bindingSource1.Position]["RelateNo"].ToString();
}
}
I think the above is not complicated at all. These are the problems I'm facing :
I press the [new] button. A row in the master is automatically added. I typed something. I click the top left cell in DataGridView2. I typed something. I hit TAB a few time or hit the down arrow key to add a new row. The first row I just finished typing dissappered. Only one empty row is left on DataGridView2. I traced it and found out DetailRow_Changing() did execute correctly, meaning that a row was added. Beacuse Table1's RelateNo didn't get to copy to Table2, making the newly added row slip away from the filter.
Similar to above. I click the top left cell in DataGridView2. I press F2 (code in KeyDown) to bring up a dialog from which I can choose a ProductId. After the dialog closed, I checked if a new DataRow is needed. If so, I execute :
DataGridView2.EndEdit();
CurrencyManager cm = (CurrencyManager)grid.BindingContext[DataGridView2.DataSource, DataGridView2.DataMember];
cm.EndCurrentEdit(); // add one DataRow
I verify that a DataRow is added by :
labelHint.Text = ds.Tables["Table2"].Rows.Count.ToString();
I try to get the DataView
DataRowView drv = (DataRowView)acDataGridView1.Rows[DataGridView2.CurrentCell.RowIndex].DataBoundItem;
if (drv == null)
MessageBox.Show("null"); // This line was executed
else
{
int i = ds.Tables["Detail"].Rows.IndexOf(drv.Row);
ds.Tables["Detail"].Rows[i]["ProductId"] = WhatEverDialogBox.GetValue("ProductId");
bindingSource3.ResetBindings(false);
}
Again, RelateNo didn't get the correct value. Everything is about default values. I found out that if a DataTable is not filtered, it can get default values correctly. What is up with filtered DataTabled ?
I put it inside bindingSource_PosionChanged(). This is a better place to give default values. But how about ModifiedTime ? It could be one hour ago.

Clearing a DataGridView which was populated from Entity Framework

Right now I have a table set up that will populate based on the searched authors last name. I want only one author to show up at a time but this current code doesn't remove the previous search result, instead it adds the next search result below the first. It seems like I need to clear the datagridview or bindingsource however I've had no luck with that.
I tried utilizing
datagridview.DataSource = null;
bindingsource.DataSource = null;
bindingsource.Clear();
in different ways before the if statement in my code with the idea that the table would be cleared before adding the next search result. What happens though is it just clears all my data after a few clicks of the button and then it won't display anything until I restart the program. I am very new to this and I've been trying to get a hang of it but it's going slow.
private void btnSearch_Click(object sender, EventArgs e)
{
if (txtSearch.Text == " ")
return;
else
{
dbcontext.Authors
.Where(author => author.LastName.Equals(txtSearch.Text))
.Load();
authorBindingSource.DataSource = dbcontext.Authors.Local.ToBindingList();
}
}
The Load method just ensures the entities are loaded locally. It's basically a dummy method that just puts the query results into EF's cache.
You're currently ensuring that the search results are in the cache, then displaying the entire cache.
You don't need to explicitly load your entities, and you don't want to display all local entities, you only want the result of your search.
I think this would work:
authorBindingSource.DataSource =
dbcontext.Authors
.Where(author => author.LastName.Equals(txtSearch.Text))
.ToList();

Access cell data in a DataGrid

Seems like a really simple thing, but all of the research and everything I have tried just doesn't seem to work. I am using VS2013 and .Net 4.5
I have a 2 column WPF DataGrid that isn't using databinding. The grid is already populated with data from a SQLCE table and I want to select a row and return as a string the data in the cell of column 0 of the selected row.
I can see that the data is there in the debug watch window
I just don't know how to access that value so that I can use it.
private void dgPickList_MouseDoubleClick( object sender, MouseButtonEventArgs e )
{
selectItem();
this.Close();
}
private void selectItem()
{
_pickedItem = dgPickList.SelectedItem.ToString();
}
This code obviously doesn't work so well but it does let me see the cell data in a Non-Public members _values field.
I really appreciate any help.
Jeff
Cast the SelectedItem back to the original type you populated the grid with.
For example, if you use a DataTable, cast to a DataRow and grab the first column (here I'm assuming it's an integer type that represents a unique ID):
var id = ((DataRow)dgPickList.SelectedItem).Field<int>(0);
If you're using a collection of a custom class, something like this should work:
var id = ((MyClass)dgPickList.SelectedItem).Id;
use DataGrid.SelectedCells instead. This will return a collection of selected cells in which you can find the column you need to read.
eg:
var celldata=dataGrid1.SelectedCells[0];
var val=celldata.Column.GetCellContent(celldata.Item);

How to access a cell in a DataRowView based on the columns DataPropertyName?

I have a Windows Forms application with a DataSet. I've simply used the Data | Add New DataSource to add the Products table of the Northwind database to my DataSources and created a DataGridView showing the contents of the Products table. I just dragged the Products table from the Data Sources window to the form, so all the columns are created automatically.
Now, I want the rows containing a product where the Discontinued column is true to be painted in a different color. I've created a CellPainting event handler for it, but I'm having trouble locating the value for the Discontinued column.
Because the DataGridView is created automatically, the Columns in it have names like dataGridViewTextBoxColumn1, which has a DataPropertyName of "ProductID".
My question is: how can I find the value for Discontinued based on the DataPropertyName? Or am I required to use the name of the column itself? (In which case I better give it a meaningful name)
My code is:
private void productsDataGridView_CellPainting(object sender,
DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex >= 0)
{
var row = productsDataGridView.Rows[e.RowIndex];
if ((bool) (row.Cells[ nameOrIndexOfColumn ].Value))
{
e.CellStyle.ForeColor = Color.DarkGray;
}
}
}
How can I access the Value of the Column with DataPropertyName "Discontinued"?
Solution
Based on Neil Barnwell's answer, this seems to be a way.
private void productsDataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex >= 0)
{
var productView = (DataRowView) productsDataGridView.Rows[e.RowIndex].DataBoundItem;
var product = productView.Row as NorthwindDataSet.ProductsRow;
if (product != null && product.Discontinued)
{
e.CellStyle.ForeColor = Color.DarkGray;
}
}
}
The big advantage of this is that the Discontinued value doesn't have to be an actual column on the DataGridView.
Don't get the value from the column in the grid, get the value from the actual datarow that populates the gridrow. This way you can avoid all the magic strings etc.
There's a little bit of casting to be done because the [Type]DataRow is hidden inside a DataView that's attached to the grid, but it is a more elegant approach (and far less brittle in case of future changes) if you integrate it nicely into your code than relying on magic strings.
Here's an old blog post of mine that describes in detail how to do it:
http://koder.wordpress.com/2010/04/09/getting-data-from-a-winforms-datagridview/
UPDATE
You've mentioned that you're using Northwind and that you've "simply dragged the Products table to the form", so I'm guessing this isn't a mission-critical piece of software, but for the benefit of other people reading, I just wanted to suggest that this wouldn't be a typical approach any more in a real application.
Typically these days we'd consider having a Domain Model, perhaps using an ORM to obtain our Domain Objects from the data store (of course datasets aren't a real ORM), then possibly using things like MVVM to build data structures optimised for binding to UI elements from those Domain Objects.
Using this approach, because you have the actual data to hand in your ViewModel, you can calculate such rules as colours etc from the real data, and the UI simply displays the results of applying those business rules.
Why don't you try giving a name to that particular column yourself and then you know how to access it. Visual studio shouldn't have any problem with this even if its auto generated. Because it was generated to help you do all the binding and column creating stuff in the background. But it is still available for editing, autogenerated doesn't mean it generated at run-time . Its just helping you do the usual stuff, but you can still edit it.
row.Cells[ nameOrIndexOfColumn ]
You can try something on these lines
if (dataGridView1.Columns[e.ColumnIndex].DataPropertyName == "Discontinued")
{
if (dataGridView1[reqdColumnIndex, e.RowIndex].FormattedValue as bool)
{
e.CellStyle.ForeColor = Color.Gray;
}
}
reqdColumnIndex is the column which has your bool value

How to create LookUp fields in DataGridView?

In my DataGridView I'am displaying a buch of columns from one table. In this table I have a column which points to item in another table. As you may already guessed, I want to display in the grid in one column some text value from the second table instead of and ItemID.
I could not find a right example on the net how to do this.
Lets assume that I have two tables in databes:
Table Users:
UserID UserName UserWorkplaceID
1 Martin 1
2 John 1
3 Susannah 2
4 Jack 3
Table Workplaces:
WorkplaceID WorkplaceName
1 "Factory"
2 "Grocery"
3 "Airport"
I have one untyped dataset dsUsers, one binding source bsUsers, and two DataAdapters for filling dataset (daUsers, daWorkplaces).
Code which I am performing:
daUsers.Fill(dsUsers);
daWorkplaces.Fill(dsUsers);
bsUsers.DataSource = dsUsers.Tables[0];
dgvUsers.DataSource = bsUsers;
At this point I see in my dgvUsers three columns, UserID, UserName and UserWorkplaceID. However, instead of UserWorkplaceID and values 1,2,3 I would like to see "Factory", "Grocery" and so on...
So I've added another column to dgvUsers called "WorkplaceName" and in my code I am trying to bind it to the newly created relation:
dsUsers.Relations.Add("UsersWorkplaces", dsUsers.Tables[1].Columns["WorkplaceID"], dsUsers.Tables[0].Columns["UserWorkplaceID"]);
WorkplaceName.DataPropertyName = "UsersWorkplaces.WorkplaceName";
Unfortunately that doesn't work. Relation is created without errors but fields in this column are empty after running the program.
What I am doing wrong?
I would like to also ask about an example with LookUp combobox in DataGridView which allow me to change the UserWorkplaceID but instead of numeric value it will show a tex value which is under WorkplaceName.
Thanks for your time.
In my opinion, the best decision would be to use the DataGridViewComboBoxColumn column type. If you do it, you should create a data adapter with lookup data beforehand and then set DataSource, DataPropertyName, DisplayMember, and ValueMember properties of the DataGridViewComboBoxColumn. You could also set the DisplayStyle property to Nothing to make the column look like a common data column. That's it.
I don't know if you can do exactly what you want, which seems to be binding the DataGridView to two different DataTable instances simulataneously. I don't think the DataGridView class supports that -- or if it does it's a ninja-style move I haven't seen.
Per MSDN, your best bet is probably using the CellFormatting event on the DataGridView and check for when the cell being formatted is in the lookup column, then you could substitute your value from the other table. Use an unbound column for the WorkplaceName column, hide the UserWorkplaceID column and then implement the CellFormatting event handle to look up the value in the row, e.g.:
private void dgv_CellFormatting(object sender,
DataGridViewCellFormattingEventArgs e)
{
if (dgv.Columns[e.ColumnIndex].Name.Equals("WorkplaceName")
{
// Use helper method to get the string from lookup table
e.Value = GetWorkplaceNameLookupValue(
dataGridViewScanDetails.Rows[e.RowIndex].Cells["UserWorkplaceID"].Value);
}
}
If you've got a lot of rows visible, this might impact performance but is probably a decent way to get it working.
If this doesn't appeal to you, maybe use the DataTable.Merge() method to merge your lookup table into your main table. A quick glance at one of my ADO.NET books suggests this should work, although I have not tried it. But I'm not sure if this is too close to the idea suggested previously which you shot down.
As for your second question about the lookup combobox, you should really post it in a separate question so it gets proper attention.
You could make SQL do the job instead. Use a join to return a table with Workplace names instead of IDs, output that table into a dataset and use it instead.
eg.
SELECT A.UserID, A.UserName, B.WorkplaceID
FROM Users A
JOIN Workplaces B ON A.UserWorkplaceID = B.WorkplaceID
Then use its output to fill dsUsers.

Categories