Accessing deleted rows from a DataTable - c#

I have a parent WinForm that has a MyDataTable _dt as a member. The MyDataTable type was created in the "typed dataset" designer tool in Visual Studio 2005 (MyDataTable inherits from DataTable) _dt gets populated from a db via ADO.NET. Based on changes from user interaction in the form, I delete a row from the table like so:
_dt.FindBySomeKey(_someKey).Delete();
Later on, _dt is passed by value to a dialog form. From there, I need to scan through all the rows to build a string:
foreach (myDataTableRow row in _dt)
{
sbFilter.Append("'" + row.info + "',");
}
The problem is that upon doing this after a delete, the following exception is thrown:
DeletedRowInaccessibleException: Deleted row information cannot be accessed through the row.
The work around that I am currently using (which feels like a hack) is the following:
foreach (myDataTableRow row in _dt)
{
if (row.RowState != DataRowState.Deleted &&
row.RowState != DataRowState.Detached)
{
sbFilter.Append("'" + row.info + "',");
}
}
My question: Is this the proper way to do this? Why would the foreach loop access rows that have been tagged via the Delete() method??

The Delete method marks a row for deletion; the row is not actually removed until you call AcceptChanges.
Instead, call _dt.Rows.Remove(_dt.FindBySomeKey(_someKey)), which will also accept the change.
Believe it or not, Rows.Remove will completely remove the row, whereas row.Delete() won't.
Note that if you call Rows.Remove, the row will be permanently gone, and will not be deleted from the database by a DataAdapter.
In C# 3, you can replace your if statement with the following extension method:
///<summary>Gets the rows in a typed DataTable that have not been deleted.</summary>
public static EnumerableRowCollection<TRow> CurrentRows<TRow>(this TypedTableBase<TRow> table) where TRow : DataRow {
return table.Where(r => r.RowState != DataRowState.Deleted);
}
foreach (myDataTableRow row in _dt.CurrentRows())
{
...
EDIT
Here's a C# 2 version:
static class Utils {
public static IEnumerable<TRow> CurrentRows(IEnumerable<TRow> table) where TRow : DataRow {
foreach(TRow row in table) {
if (row.RowState != DataRowState.Deleted)
yield return row;
}
}
}
foreach (myDataTableRow row in Utils.CurrentRows(_dt))
{
You could also put this function in the partial class for the typed table:
partial class MyTable {
public IEnumerable<MyRow> CurrentRows() { return Utils.CurrentRows(this); }
}

What you're doing to skip the rows in your iteration is correct, I routinely check the RowState when I loop over a DataTable that may be modified.
In some cases I believe you want the original row value before the row was marked as deleted. There is a secondary index option when retrieving a particular datarow value.
string st = dr["columnname", DataRowVersion.Original].ToString(); // c#
dim st As String = dr("columnname", DataRowVersion.Original).ToString() ' vb.net
I've gotten stuck on this one several times in the past.
As far as the GetChanges(RowState) method is concerned, don't forget to to check for a null return, if there are no rows of that RowState, the DataTable returned is null (I think it should return a table with zero rows)

You will have to check:
_dt.DefaultView.RowStateFilter
I think that the default setting won't show deleted rows, maybe you change it somewhere.
You can always create an extra RowView and control its filter and do your loop over the View.

Use GetChanges():
foreach (myDataTableRow row in _dt.GetChanges(DataRowState.Added))
{
sbFilter.Append("'" + row.info + "',");
}
If you want to get all added and modified, use bitwise OR on Added and Modified:
foreach (myDataTableRow row in
_dt.GetChanges(DataRowState.Added | DataRowState.Modified))
{
sbFilter.Append("'" + row.info + "',");
}

I'm not sure of this, but I think if you call _dt.AcceptChanges() after deleting one or more rows, you will not "see" the deleted rows when you iterate through the data tables's Rows collection.

Related

DataRow.SetField() gives a null ref exception when adding data to a column I previously deleted then added back

UPDATE
I think I have found what is causing the issue here https://stackoverflow.com/a/5665600/19393524
I believe my issue lies with my use of .DefaultView. The post thinks when you do a sort on it it is technically a write operation to the DataTable object and might not propagate changes made properly or entirely. It is an interesting read and seems to answer my question of why passing valid data to a DataRow is throwing this exception AFTER I make changes to the datatable
UPDATE:
Let me be crystal clear. I have already solved my problem. I would just like to know why it is throwing an error. In my view the code should work and it does.. the first run through.
AFTER I have already deleted the column then added it back (run this code once)
When I debug my code line by line in Visiual studio and stop at the line:
data.Rows[i].SetField(sortColumnNames[k], value);
the row exists
the column exisits
value is not null
sortColumnNames[k] is not null and contains the correct column name
i is 0
Yet it still throws an exception. I would like to know why. What am I missing?
Sorry for the long explanation but this one needs some context unfortunately.
So my problem is this, I have code that sorts data in a DataTable object by column. The user picks the column they want to sort by and then my code sorts it.
I ran into an issue where I needed numbers to sort as numbers not strings (all data in the table is strings). eg (string sorting would result in 1000 coming before 500)
So my solution was to create a temporary column that uses the correct datatype so that numbers get sorted properly and the original string data of the number remains unchanged but is now sorted properly. This worked perfectly. I could sort string numeric data as numeric data without changing the formatting of the number or data type.
I delete the column I used to sort afterwards because I use defaultview to sort and copy data to another DataTable object.
That part all works fine the first time.
The issue is when the user needs to do a different sort on the same column. My code adds back the column. (same name) then tries to add values to the column but then I get a null reference exception "Object not set to an instance of an object"
Here is what I've tried:
I've tried using AcceptChanges() after deleting a column but this did nothing.
I've tried using column index, name, and column object returned by DataTable.Columns.Add() in the first parameter of SetField() in case it was somehow referencing the "old" column object I deleted (this is what I think the problem is more than likely)
I've tried changing the value of the .ItemArray[] directly but this does not work even the first time
Here is the code:
This is the how the column names are passed:
private void SortByColumn()
{
if (cbAscDesc.SelectedIndex != -1)//if the user has selected ASC or DESC order
{
//clears the datatable object that stores the sorted defaultview
sortedData.Clear();
//grabs column names the user has selected to sort by and copies them to a string[]
string[] lbItems = new string[lbColumnsToSortBy.Items.Count];
lbColumnsToSortBy.Items.CopyTo(lbItems, 0);
//adds temp columns to data to sort numerical strings properly
string[] itemsToSort = AddSortColumns(lbItems);
//creates parameters for defaultview sort
string columnsToSortBy = String.Join(",", itemsToSort);
string sortDirection = cbAscDesc.SelectedItem.ToString();
data.DefaultView.Sort = columnsToSortBy + " " + sortDirection;
//copies the defaultview to the sorted table object
sortedData = data.DefaultView.ToTable();
RemoveSortColumns(itemsToSort);//removes temp sorting columns
}
}
This is where the temp columns are added:
private string[] AddSortColumns(string[] items)//adds columns to data that will be used to sort
//(ensures numbers are sorted as numbers and strings are sorted as strings)
{
string[] sortColumnNames = new string[items.Length];
for (int k = 0; k < items.Length; k++)
{
int indexOfOrginialColumn = Array.IndexOf(columns, items[k]);
Type datatype = CheckDataType(indexOfOrginialColumn);
if (datatype == typeof(double))
{
sortColumnNames[k] = items[k] + "Sort";
data.Columns.Add(sortColumnNames[k], typeof(double));
for (int i = 0; i < data.Rows.Count; i++)
{
//these three lines add the values in the original column to the column used to sort formated to the proper datatype
NumberStyles styles = NumberStyles.Any;
double value = double.Parse(data.Rows[i].Field<string>(indexOfOrginialColumn), styles);
bool test = data.Columns.Contains("QtySort");
data.Rows[i].SetField(sortColumnNames[k], value);//this is line that throws a null ref exception
}
}
else
{
sortColumnNames[k] = items[k];
}
}
return sortColumnNames;
}
This is the code that deletes the columns afterward:
private void RemoveSortColumns(string[] columnsToRemove)
{
for (int i = 0; i < columnsToRemove.Length; i++)
{
if (columnsToRemove[i].Contains("Sort"))
{
sortedData.Columns.Remove(columnsToRemove[i]);
}
}
}
NOTE:
I've been able to fix the problem by just keeping the column in data and just deleting the column from sortedData as I use .Clear() on the sorted table which seems to ensure the exception is not thrown.
I would still like an answer though as to why this is throwing an exception. If I use .Contains() on the line right before the one where the exception is thrown is says the column exists and returns true and in case anyone is wondering the params sortColumnNames[k] and value are never null either.
Your problem is probably here:
private void RemoveSortColumns()
{
for (int i = 0; i < data.Columns.Count; i++)
{
if (data.Columns[i].ColumnName.Contains("Sort"))
{
data.Columns.RemoveAt(i);
sortedData.Columns.RemoveAt(i);
}
}
}
If you have 2 columns, and the first one matches the if, you will never look at the second.
This is because it will run:
i = 0
is i < columns.Count which is 2 => yes
is col[0].Contains("sort") true => yes
remove col[0]
i = 1
is i < columns.Count which is 1 => no
The solution is to readjust i after the removal
private void RemoveSortColumns()
{
for (int i = 0; i < data.Columns.Count; i++)
{
if (data.Columns[i].ColumnName.Contains("Sort"))
{
data.Columns.RemoveAt(i);
sortedData.Columns.RemoveAt(i);
i--;//removed 1 element, go back 1
}
}
}
I fixed my original issue by changing a few lines of code in my SortByColumn() method:
private void SortByColumn()
{
if (cbAscDesc.SelectedIndex != -1)//if the user has selected ASC or DESC order
{
//clears the datatable object that stores the sorted defaultview
sortedData.Clear();
//grabs column names the user has selected to sort by and copies them to a string[]
string[] lbItems = new string[lbColumnsToSortBy.Items.Count];
lbColumnsToSortBy.Items.CopyTo(lbItems, 0);
//adds temp columns to data to sort numerical strings properly
string[] itemsToSort = AddSortColumns(lbItems);
//creates parameters for defaultview sort
string columnsToSortBy = String.Join(",", itemsToSort);
string sortDirection = cbAscDesc.SelectedItem.ToString();
DataView userSelectedSort = data.AsDataView();
userSelectedSort.Sort = columnsToSortBy + " " + sortDirection;
//copies the defaultview to the sorted table object
sortedData = userSelectedSort.ToTable();
RemoveSortColumns(itemsToSort);//removes temp sorting columns
}
}
Instead of sorting on data.DefaultView I create a new DataView object and pass data.AsDataView() as it's value then sort on that. Completely gets rid of the issue in my original code. For anyone wondering I still believe it is bug with .DefaultView in the .NET framework that Microsoft will probably never fix. I hope this will help someone with a similar issue in the future.
Here is the link again to where I figured out a solution to my problem.
https://stackoverflow.com/a/5665600

C# report edit column values

I'm generating a report based on a database table. In the table I have genders saved as either 0 or 1 (male/female). In the report I would like to show the string value rather than showing the numbers. I tried messing with the code, ie.
this.ItemsTableAdapter.Fill(this.InventoryDataSet.Items);
foreach (DataRow row in this.InventoryDataSet.Items.Rows)
{
if (row["gender"].ToString() == "0")
{
row["gender"] = "Male";
}
}
this.reportViewer1.RefreshReport();
However this breaks the report complelty (no data is shown). How can I achieve what I want?
[edit]
I came a little closer to the solution (I think) with this:
this.InventoryDataSet.Items.Columns.Add("genderVerbose", typeof(string));
foreach (DataRow row in this.InventoryDataSet.Items.Rows)
{
if (row["gender"].ToString() == "0")
{
row["genderVerbose"] = "Male";
}
}
However now I get this error when trying to use the field in the report:
Error 2 The Value expression for the text box ‘color’ refers to the
field ‘genderVerbose’. Report item expressions can only refer to
fields within the current dataset scope or, if inside an aggregate,
the specified dataset scope. Letters in the names of fields must use
the correct case.
How about assigning Enum values to the cells? Enum.ToString() by default gives the value name in code as the string. So with:
public enum Gender { Male, Female }
You can just shove those into DataGridView cells and it will show that way. You can test values by
if ((Gender)row["gender"].Value == Gender.Male)
{
// do things...
}
If you need this to be user-selectable, you can use the DataGridViewComboBoxCell.

Can I add DataTable in DataGridView without using DataSource property

Here is method which copy DataTable into DataGridView which is not not working, this method only able to add columns and empty rows in DataGridView. Can any one suggest me solution for this without using DataSource property of DataGridView?
public void CopyDataTableInDataGridView(DataTable table, DataGridView gdv)
{
if (gdv.Rows.Count > 0)
gdv.Rows.Clear();
if (table != null && table.Rows.Count > 0)
{
foreach (DataColumn _colm in table.Columns)
{
DataGridViewColumn _col;
if (_colm.GetType() == typeof(bool))
_col = new DataGridViewCheckBoxColumn();
else
_col = new DataGridViewTextBoxColumn();
_col.Name = _colm.ColumnName;
_col.HeaderText = string.Concat(_colm.ColumnName.Select(x => Char.IsUpper(x) ? " " + x : x.ToString())).TrimStart(' ');
gdv.Columns.Add(_col);
}
foreach (DataRow _row in table.Select())
{
//Rows getting added in dgv but not data
// By adding following line in Code my problem get solved
//object[] _items = _row.ItemArray;
gdv.Rows.Add(_row);
}
}
}
You are trying to add a DataRow to DataGridView instead of adding DataGridViewRow
Look at what visualstudio's intelisence is telling you about DataGridView.Rows.Add() methods. There are 4 of them:
Add() - adds an empty row to DataGridView
Add(DataGridViewRow) - adds new row (this is what you need)
Add(count) - adds [count] of empty rows to DataGridView
Add(object[]) adds new row and populates it with values (you can also use this)
you are currently using the last one: Add(object[]). compiler doesn't complain because it's treating DataGridViewRow you passed to it as an array of objects with only one object in it. Obviously not what you want.
here is related question: https://stackoverflow.com/a/9094325/891715
By adding following line before adding row in DataGridView my problem get solve.
object[] _items = _row.ItemArray;

How do I load a list via a data set from SQL Server into ListView?

I have what seems to be a simple question but its killing me trying to find out.
I have a form in which I have a ListView. In this ListView I would like to populate it with data from a SQL Server 2008 database table.
public void LoadList()
{
DataTable dtable = budget_MainDataSetReceipt.Tables["Receipt"];
listView1.Items.Clear();
for (int i = 0; i < dtable.Rows.Count; i++)
{
DataRow drow = dtable.Rows[i];
if (drow.RowState != DataRowState.Deleted)
{
ListViewItem lvi = new ListViewItem(drow["ReceiptID"].ToString());
lvi.SubItems.Add(drow["DateCleared"].ToString());
lvi.SubItems.Add(drow["CategoryID"].ToString());
lvi.SubItems.Add(drow["Amount"].ToString());
lvi.SubItems.Add(drow["Store"].ToString());
lvi.SubItems.Add(drow["DateEntered"].ToString());
listView1.Items.Add(lvi);
}
}
}
I keep getting an
Object reference not set to an instance of an object
error, and I can't figure out why. There are about 5 rows of data in my database, so in my mind, there should be 5 rows of data within the list view.
Can anyone tell me what I am missing? I can post more code if that would be helpful.
I have tried calling the LoadList() method in several ways:
Before the method itself
With the InitializeComponent() method
I have tried the following syntax
this.LoadList();
this.Form1.LoadList();`
I have also tried to initialize the DataTables type with the following:
DataTables dt = new DataTables //did not work
My hunch would be: you're assuming for all columns in your DataRow that they're present and not null - that's a bit of a dangerous assumption.
I would change your assignments to use a method that checks for DBNull before returning the string:
public string SafeGetString(DataRow row, string columnName)
{
if(row[columnName] != null && row[columnName] != DBNull.Value)
{
return row[ColumName].ToString();
}
return string.Empty;
}
so your could would look like:
ListViewItem lvi = new ListViewItem(SafeGetString(drow, "ReceiptID"));
lvi.SubItems.Add(SafeGetString(drow, "DateCleared"));
// and so forth
This way, if any of the columns should contain a NULL, you would get back an empty string - instead of running into a NULL.ToString() that causes the error you're seeing.

What is the most simple way to bind a single row of a datatable to a detailsview in C# asp.net?

I currently copy the row to an empty datatable and bind that one, but surely there is a better way...
You don't need a data-table to bind - you just need something like a list / enumerable. For example, if you know the row number:
DataRowView row = dt.DefaultView[1]; // second row
detailsView1.DataSource = new DataRowView[] {row};
detailsView1.DataBind();
Note that we have to use DataRowView (rather than DataRow) in order to get the runtime-only properties (i.e. the data from columns). If you have a DataRow, this approach could easily be wrapped in a utility method, for example an extension method:
public static DataRowView[] ForBinding(this DataRow row)
{
foreach (DataRowView rowView in row.Table.DefaultView)
{
if (ReferenceEquals(rowView.Row, row))
{
return new DataRowView[] { rowView };
}
}
throw new ArgumentException("Row not found in the default view", "row");
}
with:
detailsView1.DataSource = row.ForBinding();
detailsView1.DataBind();
yes, it is the most simple way :)
IMHO, If you want to display only 1 field, you might use output parameters, but if you display more than one field in a detailsview it'!s the most simple way.

Categories