Globally Declared DataTable Already Belongs to Another DataSet - c#

I am having problems understanding some logic but I am excited to learn what is goin on. I have an application that uses a third party web service to execute xml and receive response back, no problems here. I have some DataSets and DataTables that are declared globally. The reason I have done this is because these DataSets and DataTables will not change but need to be accessed from other methods. What happens is the form loads and my DataGridView populates just fine but when I select a different date from myComboBox the code throws an exception stating that the DataTable already belongs to another DataSet. Here is a simplified sample of what I am working with:
public class Test
{
private BusinessLayer businessLayer;
private int id;
private List<int> employees;
private DataSet employeeInfoDataSet;
private DataSet employeesTimeDataSet;
private DataTable employeeInfoDataTable;
private DataTable employeesTimeDataTable;
public Test()
{
businessLayer = new BusinessLayer();
id = 3;
// these should never change
// I almost thought about making them static
employees = businessLayer.getEmployees(id);
employeeInfoDataSet = businessLayer.getEmployeeInfoDataSet(employees);
employeeInfoDataTable = businessLayer.getEmployeeInfoDataTable(employeeInfoDataSet);
employeeInfoDataTable.TableName = "EmployeeInfo";
string date = myComboBox.SelectedValue.ToString();
initDataTable(date);
bindDataGridView();
}
private void initDataTable(string date)
{
employeesTimeDataSet = businessLayer.getEmployeesTime(employees, date);
employeesTimeDataSet.Tables.Add(employeeInfoDataTable); // <-- errors here
employeesTimeDataTable = businessLayer.buildEmployeesTimeDataTable(employeesTimeDataSet);
}
private void bindDataGridView()
{
dgv.DataSource = timesheetsDataTable;
}
private void myComboBox_SelectionChangeCommitted(object sender, EventArgs e)
{
string date = myComboBox.SelectedValue.ToString();
initDataTable(date);
bindDataGridView();
}
}
I am struggling to understand why it runs fine when the form loads but throws this exception when I change the date. Can someone help me understand what is causing this? Many, many thanks!
PS: The businessLayer.getEmployeeInfoDataTable and businessLayer.buildEmployeesTimeDataTable methods build the DataTables programmatically so there shouldn't be the "return ds.Tables[0]" issue where you need to use DataTable.Copy()...thanks :)

The error-message is self-explanatory, isn't it? You are trying to add the same table to another DataSet which you've already added to one in the constructor. You cannot add the same table(same reference) to two different DataSets. So what are you trying to do?
Maybe you want to check if it's already in a DataSet:
if( employeeInfoDataTable.DataSet == null)
employeesTimeDataSet.Tables.Add(employeeInfoDataTable);
Maybe you want to include this table into the DataSet in the first place which seems to be the best option if possible.
Or maybe you want to remove the table from the DataSet before you create it again. On this way the DataSet property of the table will be "cleared"(you cannot assign null) and you can add the table later to the newly created DataSet.
So like this:
private void initDataTable(string date)
{
employeesTimeDataSet.Tables.Remove(employeeInfoDataTable);
employeesTimeDataSet = businessLayer.getEmployeesTime(employees, date);
employeesTimeDataSet.Tables.Add(employeeInfoDataTable); // <-- now it works
employeesTimeDataTable = businessLayer.buildEmployeesTimeDataTable(employeesTimeDataSet);
}
Note that this exception is similar to the one that is raised if you're trying to add the same table twice to the same DataSet. Both is checked and not allowed.

Related

How to update edits in DataGridView to table

I have bound a DataGridView to an SQL Server table in a .Net 5.0 WinForms project. Displaying the data works well.
I would like to update editions to the database as soon as I move to another row in the DataGridView. But I have not found a way to do this.
The solution presented here seems not to work with an OleDbDataAdapter. The Update method does not accept a DataRow. Examples in DOCS require a DataSet which I try to avoid. Other examples use a button to save changes.
The data gets loaded like this:
var dataAdapter = new OleDbDataAdapter(sqlQueryString, connString);
var dataTable = new DataTable();
dataAdapter.Fill(dataTable); // fill data table from SQL server
var bindingSource = new BindingSource();
bindingSource.PositionChanged += new System.EventHandler(bindingSource_PositionChanged);
bindingSource.DataSource = dataTable; // connect binding source to data table
dataGridView.DataSource = bindingSource; // connect DataGridView to binding source
For the update I finally have tried this:
private void bindingSource_PositionChanged(object sender, EventArgs e)
{
DataRow dataRow = ((DataRowView)((BindingSource)sender).Current).Row;
if (dataRow.RowState == DataRowState.Modified) // this is successful
{
dataAdapter.Update(dataRow); // compile error
}
}
I get the compile error
Cannot convert from 'System.Data.DataRow' to 'System.Data.DataRow[]'.
Any hint is appreciated.
MVVM
In modern programming, there is the tendency to separate the model from the view. This separation makes it easier to change the way that your data is displayed without having to change your model. You can also change parts of the model without having to change the display. It is easier to reuse the model and to unit test it without having to start a forms program.
In WPF this separation between model and view is almost enforced. When using winforms you have to take care that you do not mix them more than needed.
To keep these two separated, adapter code is needed to glue your model to your view. This adapter code is quite often called the viewmodel. the abbreviation of these three is quite often called MVVM. Consider to familiarize yourself with the ideas of MVVM.
Use a BindingList in your DataSource
If you want to separate your model from your view, you need methods to fetch the data that must be displayed from the database, and data to update items.
I don't know what you will be displaying in your DataGridView, but let's assume it is a sequence of Products, something like this:
class Product
{
public int Id {get; set;}
public string ProductCode {get; set;}
public string Name {get; set;}
public string Description {get; set;}
public decimal Price {get; set;}
...
}
You will have methods to fetch the Products that must be displayed, and to Update one Product, or maybe several Products at a time:
IEnumerable<Product> FetchProductsToDisplay(...)
{
// TODO: fetch the products from the database.
}
void UpdateProduct(Product product) {...}
void UpdateProducts(IEnumerable<Product> products) {...}
Implementation is out of scope of this question. By the way, did you notice, that because I put fetching and updating data in separate procedures, I hid where the Products are saved? It can be in an SQL server, but if you want it could also be a CSV or XML file, or even a dictionary, which could be handy for unit tests.
Besides: you can unit tests these methods without using your forms.
Using the visual studio designer you have added the columns and defined which column should show which Product property. You could also have done this in the constructor using property DataGridViewColumn.DataPropertyName
public MyForm()
{
InitializeComponents();
this.columnProductCode.DataPropertyName = nameof(Product.ProductCode);
this.columnName.DataPropertyName = nameof(Product.Name);
...
}
You don't need to set the DataPropertyName for properties that you won't show anyway.
Now to display the products, it is enough to assign the Products to the DataSource of the DataGridView:
var productsToDisplay = this.FetchProductsToDisplay(...);
this.dataGridView1.DataSource = productsToDisplay.ToList();
This will display the products. However, changes that the operator makes: Add / Remove / Edit rows are not updated. If you need this functionality, then the Products need to put in an object that implements IBindingList, like (surprise!) BindingList<Product>:
private BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.dataGridView1.DataSource;
set => this.dataGridView1.DataSource = value;
}
To Initialize the DataGridView:
private void DisplayProducts()
{
this.DisplayedProducts = new BindingList<Product>(this.FetchProductsToDisplay().ToList());
}
Now whenever the operator makes any change to the DataGridView: Add / Remove rows, or change the Displayed values in a row, these changes are reflected in DisplayedProducts.
If for instance the operator clicks Apply Now to indicate he has finished editing the products:
private void OnButtonApplyNow_Clicked(object sender, ...)
{
var products = this.DisplayedProducts;
// find out which Products are Added / Removed / Changed
this.ProcessEditedProducts(products);
}
Of course you can Add / Remove / Change displayed products programmatically:
void AddProductsToDisplay()
{
Product product = this.DisplayedProducts.AddNew();
this.FillNewProduct(product);
}
Back to your question
Ask yourself: Is it wise to update the database as soon as the position is changed?
If the operator starts typing, then remembers he can copy-paste items, he will stop typing, go to other controls to copy, and then continue editing the cell by pasting. Maybe he goes to other rows to look at information to decide what to put in the cell.
Another scenario: the Descriptions of Product A and Product B need to be exchanged. Think of the operator actions needed for this. When would it be wise to update the database? When are you certain that the operator is content with the new data?
Hence it is not wise to update the database as soon as a row is edited. The operator should explicitly indicate he has finished editing.
private void OnButtonOk_Clicked(object sender, ...)
{
var products = this.DisplayedProducts;
// find out which Products are Added / Removed / Changed
this.ProcessEditedProducts(products);
}
Further improvements
Once you've separated your data (model) from the way this data is displayed (view), using the DataSource, it is quite easy to access the Product that is displayed in the current row or in the selected rows:
Product CurrentProduct => (Product) this.dataGridView1.CurrentRow?.DataBoundItem;
IEnumerable<Product> SelectedProducts = this.dataGridView1.SelectedRows
.Cast<DataGridViewRow>()
.Select(row => row.DataBoundItem)
.Cast<Product>();
you can use foreach loop.
private void AddInfo()
{
// flag so we know if there was one dupe
bool updated = false;
// go through every row
foreach (DataGridViewRow row in dgv_Purchase.Rows)
{
// check if there already is a row with the same id
if (row.Cells["Pro_ID"].ToString() == txt_ProID.Text)
{
// update your row
row.Cells["Purchase_Qty"] = txt_Qty.Text;
updated = true;
break; // no need to go any further
}
}
// if not found, so it's a new one
if (!updated)
{
int index = dgv_Purchase.Rows.Add();
dgv_Purchase.Rows[index].Cells["Purchase_Qty"].Value = txt_Qty.Text;
}
}
Finally I've found the 2 missing lines:
private SqlCommandBuilder commandBuilder; // on UserControl level
commandBuilder = new SqlCommandBuilder(dataAdapter); // when loading data
A book has helped me: Michael Schmalz, C# Database Basics, O'Reilly
It is strange that the DOCS reference of SqlDataAdapter doesn't mention the SqlCommandBuilder.
Thanks to everybody who has spent precious time for a New contributor.

Can we use same datatable for pagemethod and webmethod in ASP.NET?

I am trying to create a new webpage where i need to display almost 10 different gridviews and charts.
Gridviews are binded on pageload event and charts are displayed using jquery-ajax method (using amcharts as well as highcharts) by calling WebMethod.
Initially i implemented the page in a way that after executing same set of stored procedures for gridviews(for showing grid view data) and webmethods(for drawing charts).So same sps are executed twice for this page(one for grid and another for chart).There are 10 sps required to execute for fetching the data.
So for improving the page performance i have created static datatable like this
static DataTable Report1;
and binded the gridview like this.
private void gvbindReport1()
{
try
{
Report1 = new DataTable();//refreshed datatable
DataSet ReportDS1 = objmvbl.GetReportGraph(ClientID, date_From, date_To);
if (ReportDS1.Tables.Count > 0)
{
Report1 = ReportDS1.Tables[0];//bindinding data to static datatable
}
GdReport.DataSource = Report1;
GdReport.DataBind();
}
catch (Exception ex)
{
Log.Errlog("Error Occured in gvbindReport1 : " + ex.Message.ToString());
}
}
and inside the webmethod i have used the same datatable for drawing the chart
like this
[System.Web.Services.WebMethod]
public static string GetDataReport1()
{
System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
List<Dictionary<string, object>> rows = new List<Dictionary<string, object>>();
Dictionary<string, object> row;
try
{
//processing for the data inside static datatable
if (Report1.Rows.Count > 0)
{
foreach (DataRow dr in Report1.Rows)
{
row = new Dictionary<string, object>();
foreach (DataColumn col in Report1.Columns)
{
row.Add(col.ColumnName, dr[col]);
}
rows.Add(row);
}
}
}
catch (Exception ex)
{
Log.Errlog("Error Occured in GetDataReport WebMethod of Report Page : " + ex.Message.ToString());
}
return serializer.Serialize(rows);
}
with this i am able to show both grid and charts.
Now Please tell me that, is this a correct approach to deal with webmethods? i have read that webmethod have no relation to the page and all.Please Tell me the drawbacks of this method.
If this is wrong,Please suggest a better way to improve the page performance?
No, this is not the correct method. Since you have declared the DataTable as static (a static variable has application scope and cannot be instantiated) all
users will get the same result (last updated values).
You can realize this in concurrency testing.
Please check the following scenario:
Consider dtbl is the static dataTable which is initialized on the home page, and you create another instance of `datatable on the index page (both are in page load as given below).
Home
public static DataTable dtbl;
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
dtbl = new DataTable();
dtbl.Columns.Add("id");
dtbl.Columns.Add("name");
for (int i = 0; i < 10; i++)
{
DataRow dr = dtbl.NewRow();
dr["id"] = i.ToString();
dr["name"] = i + 1;
dtbl.Rows.Add(dr);
}
}
}
Index page
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
home.dtbl = new DataTable();
}
}
Now put a breakpoint in each page load and run the application,
Open both the pages in separate tab.
Refresh the home page and check whether the columns are showing
Now go to the next tab (index) and refresh it (a new instance is created for dt). It will affect the data table now you will get the new data table at home also.
So if these two processes/pages are concurrently executed the latest value will get for both the pages. That's why I am saying it will realize this in concurrency testing.
You can make use of a session in this case. Consider the following code:
Home
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
dtbl = new DataTable();
dtbl.Columns.Add("id");
dtbl.Columns.Add("name");
for (int i = 0; i < 10; i++)
{
DataRow dr = dtbl.NewRow();
dr["id"] = i.ToString();
dr["name"] = i + 1;
dtbl.Rows.Add(dr);
}
if (((DataTable)Session["MyDatatable"]).Columns.Count < 0)
{
Session["MyDatatable"] = dtbl;
}
else
{
dtbl = (DataTable)Session["MyDatatable"];
}
}
}
First off, do not use, as a general rule of thumb, static variables in an web application. These act as global variables and are not instantiated with each request.
I wouldn't also suggest you using DataTables all the way up to your UI layer. Instead, work with strongly-typed objects.
Make a Model of the object you are trying to bind.
Like for example if you have a table called person that has the following fields.
Id | first_name | last_name | audit_ts
You can create an object as such:
public class Person
{
public int Id {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
}
Now in a separate functions, in some class you can call your stored procedure from the database and then cast your table rows in the person table into the list of Person Object.
Now, instead of calling your stored procedure twice to get the same data, which only reduces your application's performance, what you can do is to instead of binding your grid view in your code behind at Page_Load event. Simply bind the HTML table after you make the call to your webmethod which I believe is in your code-behind. You can refer to this post regarding how to bind your HTML table with JSON object returned by your Ajax call.
This way, you are making one call to the server and to the database to use the same data to bind your table as well as your charts.
This is a good use case for the little used Cache Object Many users understand ViewState and SessionState, however the Cache object is not as widely utilized, and although the concept is very similar, it is much more flexible.
If your page is calling 10 stored procedures twice (once for your grids and a second time for your charts) then lets improve the performance by roughly 100% by eliminating the extra calls with the Cache Object
Have one call to the stored procedures in a separate method that populate your data tables cache object, which is then reused throughout your application.
private void loadReport1IntoCache()
{
//...load your data from DB into the Report1 variable here
//this line is new, and it saves your data into a global Cache variable
//with an absolute expiration of 10 minutes
Cache.Insert("Report1", Report1, null,
DateTime.Now.AddMinutes(10d),
System.Web.Caching.Cache.NoSlidingExpiration);
}
Then, when you are inside your other methods, you can use the Cache variable instead of calling stored procedures again. For example:
[System.Web.Services.WebMethod]
public static string GetDataReport1()
{
//first load the application variable before performing your other work
DataTable myCachedReport1Data = (DataTable)Cache["Report1"];
//did the Cache expire?
if (myCachedReport1Data == null)
{
//if so refresh it
loadReport1IntoCache();
//and then assign the variable the contents of the refresh and proceed
myCachedReport1Data = (DataTable)Cache["Report1"];
}
//other work here, utilizing the myCachedReport1Data variable
}
and for your grid binding:
private void gvbindReport1()
{
try
{
DataTable myCachedReport1Data = (DataTable)Cache["Report1"];
//did the Cache expire?
if (myCachedReport1Data == null)
{
//if so refresh it
loadReport1IntoCache();
//and then assign the variable the contents of the refresh
myCachedReport1Data = (DataTable)Cache["Report1"];
}
GdReport.DataSource = myCachedReport1Data ;
GdReport.DataBind();
}
catch (Exception ex)
{
Log.Errlog("Error Occured in gvbindReport1 : " + ex.Message.ToString());
}
}
Now, you will have to do a few things not mentioned here. You should consider when you want your Cache data to expire (the example given is 10 minutes). Also you should consider if you want it to be an Absolute Number of minutes (Absolute Expiry) or a number of minutes since last access (Sliding Expiry). In your case, probably absolute expiry, but only you know that. Then you will set the expiration when you are setting the variable contents.
See the Cache documentation here:
https://msdn.microsoft.com/en-us/library/6hbbsfk6.aspx
Adding Cache data:
https://msdn.microsoft.com/en-us/library/18c1wd61.aspx
Retrieving Cache data:
https://msdn.microsoft.com/en-us/library/xhy3h9f9.aspx
Looking at the code sample that you have given (and the parameters date_from and date_to that you are passing to GetReportGraph()) I assume:
you have 2 input fields where user is specifying the date range and then submitting the data (causing postback), based on which you are filtering the records and showing in grid as well as chart.
as different users would be providing different date ranges, you don't want to show the same data to all users.
as the data is filtered, its not going to have thousands of records.
I'm not sure what functionality of grid view you are using. Is it used only to show read only tabular data? If yes, you can consider the approach given by #Nabin Karki Thapa. If not check the alternate approach below:
After you have got the data table and bound it to grid view, immediately serialize it to JSON and register it as a script block (define a JS variable and assign the serialized JSON as it's value).
On the client side, while charting, instead of invoking webmethod, to get the JSON object use the JS variable that you have registered. This way you will avoid the call to web method (AJAX) and extra stored procedure call altogether.

EF Don't save using IQueryable in BindingSource

I've created a code first context with a DbSet property
I work with Windows form. If I bind as follow:
_context.Schedules.Load();
scheduleBindingSource.DataSource = _context.Schedules.Local.ToBindingList();
All works great and when I save as follow:
this.Validate();
scheduleBindingSource.EndEdit();
_context.SaveChanges();
The data persists; But when I bind the data like this:
var res = _context.Schedules.Where(k => k.EmployeeName.Equals(employeeComboBox.Text)).ToList();
scheduleBindingSource.DataSource = res;
When I save data doesn't persis!
I'm thinking that the ToList() method is not good, but I can't find alternative to get a BindingList connected to the Local set of data inside the context.
Thanks,
Andrea
You can try this:
_context.Schedules.Where(k => k.EmployeeName.Equals(employeeComboBox.Text)).Load();
scheduleBindingSource.DataSource = _context.Schedules.Local.ToBindingList();
That should only bring the schedules that meet the condition. When you call the Load method after the Where method, it is going to bring to memory only the records that meet the condition. Later, when you call the Local property,it will give you an ObservableCollection<Schedule> that contains all the objects that are currently tracked by the DbContext which thy are going to be the elements you loaded before. At the end, when you call the ToBindingList extension method, it will returns an BindingList<Schedule> that stays in sync with the given ObservableCollection<Schedules>.
The reason that caused the non-persistance of the data is caused by DataGridView (or the BindingSource), that don't add to context the new istance of the just added row.
So I've disabled the AllowUserToAddRow property (now I'm using BindingNavigator Add Button)
And implemented these two events as follow:
private void scheduleBindingSource_AddingNew(object sender, AddingNewEventArgs e)
{
_scheduleAdding = true;
}
private void scheduleBindingSource_CurrentChanged(object sender, EventArgs e)
{
if (_scheduleAdding)
{
Schedule s = (Schedule)scheduleBindingSource.Current;
s.EmployeeName = employeeComboBox.Text;
s.From = new DateTime(dateTimePicker1.Value.Year, dateTimePicker1.Value.Month, 1);
_context.Schedules.Add(s);
_scheduleAdding = false;
}
}

How to display one row of data grid in ASP.NET c# using WCF

I am working in silverlight application using WCF. I want to show specific row from the database in the data grid.
I found e.result BUT this will show the whole from DB.
Below is my code:
public TrainingBatchesUsersResults(int TraineeID)//, int TrainingProgramID, int >ReadyContainerID, int TrainingBatch, int ReadyContainerItemID)
{
InitializeComponent();
XraySimulatorWCFServices.XraySimulatorServiceClient XrayClient =new XraySimulatorWCFServices.XraySimulatorServiceClient();
XrayClient.GetTrainingBatchesUsersResultsCompleted += new EventHandler<
XraySimulatorWCFServices.GetTrainingBatchesUsersResultsCompletedEventArgs>(DisplayTrainingBatchesUsersResults);
XrayClient.GetTrainingBatchesUsersResultsAsync(TraineeID);
LayoutRoot.UpdateLayout();
}
private void DisplayTrainingBatchesUsersResults(object sender, XraySimulatorWCFServices.GetTrainingBatchesUsersResultsCompletedEventArgs e)
{
dgvResult.ItemsSource = e.Result;
dgvResult.Visibility = System.Windows.Visibility.Visible;
}
Just set your ItemsSource to the single row you want to show then, rather than the whole results collection. For example dgvResult.ItemsSource=e.Result.FirstOrDefault(x=>x.Id==theIdYouWantToShow)
BUT what you really want is to do this further up the chain so that you only RETURN the single row you want, rather than return the whole table and reduce that set on the client.
Your call XrayClient.GetTrainingBatchesUsersResultsAsync(TraineeID); presumably is returning all your rows, why not have a new method that only returns the single result you're interested in?

create instance of DataColumnChangeEventArgs in C#

I am trying to write a function as follows:
private void Func1(DataColumnChangeEventArgs e)
{
ds.TableName.AddRow(
e.Row[e.Column.ColumnName, DataRowVersion.Original].ToString(),
e.Row[e.Column.ColumnName, DataRowVersion.Proposed].ToString());
}
and I am calling it as:
private void Func2()
{
DataColumnChangeEventArgs e = new DataColumnChangeEventArgs(
dataTable.Rows[index],
dataTable.Columns["ColName"],
newValue);
e.ProposedValue = newValue;
Func1(e);
}
However, e.Row[e.Column.ColumnName, DataRowVersion.Proposed].ToString() is throwing a VersionNotFoundException. Is there any way to achieve this?
I would say, your method should look like this:
ds.TableName.AddRow(e.Row[e.Column.ColumnName].ToString(), e.ProposedValue.ToString());
As there is no versions in that row in your row in args, but there is new and old values...
DataColumnChangedEventArgs is designed to be used with a class like DataTable. The DataTable creates an instance when calling the associated events (like ColumnChanged). Creating an instance of one will not actually create the change to the data row.
The e.ProposedValue = newValue is redundant, you already gave the newValue in the constructor.
You can access the value through e.ProposedValue. So through this system you can only make one change to the row at a time and you must remember which column it was.

Categories