I'm writing a WPF application connected to a local Access database. In one of the application screens, one table data (named Service) is shown in individual textboxes, like a form, and the user can navigate through records, create new ones, delete, edit or search. Everything is done on the same table.
After a intensive research on how to navigate through records displayed in textboxes, I ended up using a DataSet and a CollectionView.
public partial class Entries : Window
{
AgendaDataSet agendaDataSet = new AgendaDataSet();
AgendaDataSetTableAdapters.ServiceTableAdapter serviceAdapter = new AgendaDataSetTableAdapters.ServiceTableAdapter();
CollectionView workSheetView;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.serviceAdapter.FillByDateAsc(agendaDataSet.Service);
this.DataContext = agendaDataSet.Service;
this.workSheetView = (CollectionView)CollectionViewSource.GetDefaultView(agendaDataSet.Service);
this.workSheetView.MoveCurrentToLast();
}
I got record navigation working using the CollectionView methods MoveCurrentToFirst(), MoveCurrentToNext(), etc. I also can create new records, edit and delete.
This is the method I use to create a new record:
private void btnNovo_Click(object sender, RoutedEventArgs e)
{
dynamic row = this.agendaDataSet.Service.NewMainRow();
this.agendaDataSet.Service.AddMainRow(row);
this.workSheetView.MoveCurrentToLast();
}
My problem is with record searching. I have a button that, when the user presses it, it asks for the PatientName he is searching. Then, the data about that Patient must appear on the various textboxes, ready to be consulted, edited or deleted.
Through the CollectionView, I only found the method GetItemAt() that gets a record based on it's row index. Since I am working with an Access database, I can't use the predicate ROW_NUMBER. And I don't think this approach would be the best.
So, how can I get an item based on it's ID, or PatientName, or any other field, and pass it as a row to the CollectionView?
Probably you don't need to get an item based on its ID or PatientName property.
Suppose that the user looks for "Andrew" as PatientName. Your code finds that the second row of your DataTable (called "Service") is the one the user is looking for.
You can use a simple static method to look for a DataRowView, something like this:
private static DataRowView FindDataRowView(DataView dataView, DataRow dataRow)
{
foreach (DataRowView dataRowView in dataView)
{
if (dataRowView.Row == dataRow)
{
return dataRowView;
}
}
return null;
}
and then you can select the object in your CollectionView:
collectionView.MoveCurrentTo(FindDataRowView(agendaDataSet.Service.DefaultView,
agendaDataSet.Service.Rows[2]));
Of course you can find the real DataRow index by using a foreach cycle or the Select method of DataTable.
Related
I fill datagrid from DataBase using sqlite - ShowDataBase(string a);
Then I want to sort It,When Button is Clicked
(Note! I want to sort it only in program(not modding the DB). Just want to delete wrong rows)
private void Button_Click(object sender, RoutedEventArgs e)
{
ShowDataBase("Pacients");
if ((bool)SortFromCheckBox.IsChecked)
{
//Delete all data earlier than data that user asked for
for (int i = DataGridMain.Items.Count - 1; i >= 0; i--)
{
DataGridRow row = (DataGridRow)DataGridMain.ItemContainerGenerator.ContainerFromIndex(i);
var item = DataGridMain.Items[i];
var data = DataGridMain.Columns[4].GetCellContent(item);
if ((Convert.ToDateTime(data)) <= (Convert.ToDateTime(SortFromTextBox.Text)))
{
//Smth to delete row № i from datagrid
}
}
}
if ((bool)SortUntilCheckBox.IsChecked)
{
//Delete all data older than data that user asked for
}
if ((bool)SortByClientCheckBox.IsChecked)
{
//Delete all data where client cell is != name that user want to sort by
}
if ((bool)SortByDoctorCheckBox.IsChecked)
{
//Delete all data where doctor cell is != name that user want to sort by
}
}
From what I understand you need a functionality to filter and sort the data. You could make use of a build in mechanics for that purpose: CollectionView. You need to create one which source will be set to the data you get from database.
To filter the collection you can use the collectionView.Filter and define different filters for your purposes. For Sorting functionality you should make use of collectionView.SortDescriptions. This way you utilize the tools already present in .NET that you only need to customize for your needs.
With this approach, you don't remove anything from base collection, so you need to get the data only once and present it in different ways in your application. If you are using MVVM approach, this code should be done in the ViewModel. If you insist on achieving such functionality in your View, you could play with <CollectionViewSource>.
You can find some resources here:
Dr.WPF: ItemsControl: 'C' is for Collection
Dr.WPF: ItemsControl: 'E' is for Editable Collection
MSDN: How to: Group, Sort, and Filter Data in the DataGrid Control
I'm using a RadGridView to display items. On a doubleclick the user comes to a detail view where he can edit the contents and then comes back (both usercontrols are displayed as tabs on screen and are thus existing simultaneously).
I'm using a working method to update the edited list in the grid view without reloading the whole gridview:
dataRow = (GridViewRowInfo)element.Data;
Worker displayedWorker = (Worker)edataRow.DataBoundItem;
Worker changedWorkerFromDataBase = GetWorkerFromDataBase(displayedWorker.WorkerNumber).FirstOrDefault();
List<Worker> tableDataSource = (List<Worker>)MyGridView.DataSource;
int indexInTableDataSource = tableDataSource.IndexOf(tableDataSource.Where(e => e.WorkerNumber == displayedWorker.WorkerNumber).First());
tableDataSource[indexInTableDataSource] = changedWorkerFromDataBase;
dataRow.InvalidateRow();
Like mentioned this code works, but when I change the IndexOf method to:
int indexInTableDataSource = tableDataSource.IndexOf(displayedWorker);
it doesn't work any longer as expected. The above code works once and only once after a second save the objects stored inside the MyGridView.DataSource and the one stored inside the dataRow.DataBoundItem are seen as being different resulting in the indexOf returning -1.
This behaviour surprised me quite a lot especially as the first method works without problems and I had thought that the dataBound item of the row is just a pointer towards the items inside the GridView datasource.
Thus my question here is: Can (after the update is done) I tell the row to update/refresh its databound item so that it is equal again to the one from the gridview (aka seen as the same object) ?
First off: If you have sorting/grouping enabled, the index in the datasource might not correspond with the index in the radgridview (see Childrows vs Rows).
I have worked on a similar scenario with a radgridview and a detailview.
To notify the radgridview that a propertyvalue is changed your Worker class needs to implement the INotifyPropertyChanged interface. About INotifyPropertyChanged
Change your datasource type from List to BindingList as this list has events raised when the list is changed. About BindingList
Now you should be able to alter the instance of your Worker class and the changes should be picked up immediatly in your radgridview.
The problem in your case is coming from the fact that for the grid's DataSource you are using List, which does not support notifications. If you switch to BindingList you will be good to go. Here is a sample:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
AddGrid();
BindingList<Worker> lst = new BindingList<Worker>() { new Worker() { ID = 1, Name = "Adam" }, new Worker() { ID = 2, Name = "Eva" } };
radGridView1.DataSource = lst;
}
class Worker
{
public int ID { get; set; }
public string Name { get; set; }
}
private void radButton1_Click(object sender, EventArgs e)
{
Worker displayedWorker = (Worker)radGridView1.CurrentRow.DataBoundItem;
BindingList<Worker> tableDataSource = (BindingList<Worker>)radGridView1.DataSource;
int indexInTableDataSource = tableDataSource.IndexOf(displayedWorker);
Worker changedWorkerFromDataBase = new Worker() { ID = 1, Name = "new name" };
tableDataSource[indexInTableDataSource] = changedWorkerFromDataBase;
}
More on the difference between List and BindingList can be found here: https://stackoverflow.com/a/2244039/298871
I have a user control that contains a GridView. I pass an IEnumerable data source object to the user control instance. How can I use a ForEach to loop through the data source in the user control code behind to display the columns I want from the data source?
So far, I have the following code in the code behind of the user control:
public IEnumerable<object> DataSource { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
this.GridView1.DataSource = DataSource;
foreach (var item in DataSource)
{
//The line below gives an error - what's the correct way to do this?
this.GridView1.Columns.Add(new BoundColumn() {DataField = "What to put here", HeaderText = "What to put here"; }
}
this.GridView1.DataBind();
}
You should not loop on all items of the DataSource, you are looping vertically on all records while you want to loop only once, horizontally, on all columns. Since you know the list of properties of the object contained in the DataSource you can do this statically and not even in a foreach. If you really want to have it dynamic then you could use Reflection and loop on all public fields of the first object available in your IEnumerable.
Edit: to find all public fields of an object via reflection see here:
How can I find all the public fields of an object in C#?
but this only applies if you want to make it generic, if you already know that your object contains some fields like Name, Address and Email, for example, you do not need it.
I have a windows forms application containing a datagridview control. The datagridview is populated by the contents of an xml file. At the moment, all of the columns are displayed as datagridviewtextboxcolumns. I want to select one that is populated by a particular xml tag and display it's content in a datagridviewcomboboxcolumn along with 2 other options.
EXAMPLE:
<SMS>
<Number>+447931663542</Number>
<DateTime>2009-07-12T17:00:02</DateTime>
<Message>YES</Message>
<FollowedUpBy>Unassigned</FollowedUpBy>
<Outcome>Resolved</Outcome>
</SMS>
The OUTCOME tag is the column that I would like to be displayed as a comboboxcolumn in the datagridview. If for example the tag is empty and contains no data, then I want to display nothing, but have the comboboxcolumn populated with 3 possible options to choose from (Unresolved, Resolved, Pending). If however the tag contains data, I want that particular item to be displayed in the comboboxcolumn, and have the other two options available to be selected.
Help in achieving this would be appreciated greatly!
Regards,
EDIT:
Currently I use this code:
colOutcome = new DataGridViewComboBoxColumn();
colOutcome.HeaderText = "Outcome";
colOutcome.Width = 90;
colOutcome.Items.AddRange("Resolved", "Unresolved", "Pending");
this.dataGridView1.Columns.Insert(1, colOutcome);
this.dataGridView1.Columns[1].Name = "OutcomeColumn";
This code above populates the combobox. THE PROBLEM IS: When The xml document populates the datagridview, the outcome column just appears as a textbox column, containing the data inbetween the outcome tags in the xml file. My point is, how can i get the datagridview to realise when it reads the outcome column that it needs to be changed into a combobox column and then display the data that way, along with the other potentially selectable options in the combobox?! Currently the datagridview gets populated with all columns as textboxcolumns containing the data, as well as a seperate combobox column which is not what I want. I need the application to merge the outcome column and its data with the code above.
Any ideas?
Updated Answer
You could pass in the XML document to a function that will loop through each node and determine whether it should be a ComboBox one or not i.e. if the name is "Outcome".
private void CreateColumns(XmlDocument doc)
{
foreach (...) // loop through each node in xml document
{
if (node.Name == "Outcome")
{
var items = new List<string>() { "Resolved", "Unresolved", "Pending" };
this.dataGridView1.Columns.Add(CreateComboBoxColumn(node.Name, items));
}
else
{
this.dataGridView1.Columns.Add(String.Format("col{0}", node.Name), node.Name);
}
}
}
Then your code for creating the Outcome column would be:
private DataGridViewComboBoxColumn CreateComboBoxColumn(string colHeaderText, List<string> items)
{
var colOutcome = new DataGridViewComboBoxColumn();
colOutcome.HeaderText = colHeaderText;
colOutcome.Width = 90;
colOutcome.Items.AddRange(items.ToArray());
colOutcome.Name = String.Format("col{0}", colHeaderText);
return colOutcome;
}
You would then just call CreateColumns on the form load event and pass in your XML. You should only need to create the columns once.
My advice would be to have a similar function that will find all the SMS elements and add a new row populating it with the information in each node.
public void MyForm_Load(object sender, EventArgs e)
{
var doc = new XmlDocument(filename);
CreateColumns(doc);
CreateRows(doc);
}
Hope that helps.
Answer #2 for me, based on the updated question.
The problem you are experiencing is with the AutoGeneratedColumns functionality of the DataGridView. You will need to create your columns manually before databinding. This can be done at design-time or run-time. I prefer design-time because it gives you a bit more direction with the look/feel of the grid but either way works.
You will need to disable the AutoGeneratedColumns property of the grid:
private void Form1_Load(object sender, EventArgs e)
{
// Define your columns at run-time here if that's what you prefer
this.dataGridView1.AutoGeneratedColumns = false;
this.dataGridView1.DataSource = myDataSource;
}
I'm not sitting in front of VS so this might not compile but should give you direction.
You need to either pre-populate the ResolvedColumn with the 3-4 possible values at design-time or assign it to another datasource at runtime. If you chose the design-time approach, simply open the DataGridView "Edit Columns" dialog, find the ResolvedColumn, go to Items, and add your values ("", "Unresolved", "Pending", "Resolved"). The empty value might help the ComboBox to render if there is the possiblity of rendering the grid with SMS records that have no Outcome.
To bind the possible options at runtime do something like this:
private List<string> _outcomeDataSource;
private void Form1_Load(object sender, EventArgs e)
{
_outcomeDataSource = new List<string>;
_outcomeDataSource.Add("");
_outcomeDataSource.Add("Unresolved");
_outcomeDataSource.Add("Pending");
_outcomeDataSource.Add("Resolved");
ResolvedColumn.DataSource = _outcomeDataSource;
ResolvedColumn.PropertyName = "Outcome";
}
[Note : I've simplified my example for clarity]
Let's say that I have a Sqlite database with two tables: Items and Sectors:
Items:
id_items : INTEGER PRIMARY KEY
name_item : VARCHAR(...)
id_sector : INTEGER
Sectors:
id_sector : INTEGER PRIMARY KEY
name_sector : VARCHAR(...)
I currently have a datagridview that is bound to the Items table. It gets fed alright, and the table displays the sector as a datagridviewcomboboxcolumns.
Hence, in my Winforms CustomControl, I have all the data loading and binding that occurs in the load() method:
colSector.DataSource = m_dataContext.SectorTable;
colSector.DisplayMember = "name_sector";
colSector.ValueMember = "id_sector";
ItemsGrid.DataSource = new DataView(m_dataContext.ItemsTable);
The comboboxes of my dataview are well loaded with the data from the Sectors table.
I now would like a button on my form that would allow the creation of a new sector:
I created a txtbox (txtNewSector) and a button that triggers the creation:
private void btnAddNewSector_Click(object sender, EventArgs e)
{
// Add new sector to db
m_dataContext.AddNewSector(newSectorName);
// refresh dataview so that comboboxes are updated with the new entry
???
}
How can I perform that refresh?
I hope the edit made the question more clear, please advise....
best regards
Instead of
???
add
ItemsGrid.DataSource = new DataView(m_dataContext.ItemsTable);
You can use a timer for that. It will accomplish the task after a certain time interval.
Have a look at the example Timer in C#.
You will find the idea of using it.
I am give some rough sketch
private DataTable LoadData()
{
DataTable dt = LoadDatabaseData();
return dt;
}
private void timer1_Tick(object sender, System.EventArgs e)
{
myDataGrid.DataSource = LoadData();
myDataGrid.Databind();
or
your combo box's datasource what ever.
}
Note- you are using SQLite regarding which I don't have any idea apart from it is a database. I have only showed you how to call the function that will be the datasource for your grid and after every 1 sec(I mean what ever time interval you specify) the datawill be refreshed.
Hope this helps.