4.6. Working with Data

The entities in the model definition contain no data. The easiest way to to load data is to call the Load method. For example,

  1. private void LoadCustomersData()
  2. {
  3. dbContext.CUSTOMERS.Load();
  4. var customers = dbContext.CUSTOMERS.Local;
  5. bindingSource.DataSource = customers.ToBindingList();
  6. }
  7. private void CustomerForm_Load(object sender, EventArgs e)
  8. {
  9. LoadCustomersData();
  10. dataGridView.DataSource = bindingSource;
  11. dataGridView.Columns["CUSTOMER_ID"].Visible = false;
  12. }

However, this approach has a few drawbacks:

  1. The Load method loads all data from the CUSTOMER table to memory at once

  2. Although lazy properties (INVOICES) are not loaded immediately, but only once they are accessed, they will be loaded anyway when the records are shown in the grid and it will happen each time a group of records is shown

  3. Record ordering is not defined

To get around these drawbacks, we will use a feature of the LINQ (Language Integrated Query) technology, LINQ to Entities. LINQ to Entities offers a simple and intuitive approach to getting data using C# statements that are syntactically similar to SQL query statements. You can read about the LINQ syntax in LINQ to Entities.

4.6.1. LINQ Extension Methods

The LINQ extension methods can return two objects: IEnumerable and IQueryable. The IQueryable interface is inherited from IEnumerable so, theoretically, an IQueryable object is also an IEnumerable. In reality, they are distinctly different.

The IEnumerable interface is in the System.Collections namespace. An IEnumerable object is a collection of data in memory that can be addressed only in a forward direction. During the query execution, IEnumerable loads all data. Filtering, if required, is done on the client side.

The IQueryable interface is in the System.Linq namespace. It provides remote access to the database and movement through the data can be bi-directional. During the process of creating a query that returns an IQueryable object, the query is optimized to minimise memory usage and network bandwidth.

The Local property returns the IEnumerable interface, through which we can create LINQ queries.

  1. private void LoadCustomersData()
  2. {
  3. var dbContext = AppVariables.getDbContext();
  4. dbContext.CUSTOMERS.Load();
  5. var customers =
  6. from customer in dbContext.CUSTOMERS.Local
  7. orderby customer.NAME
  8. select new customer;
  9. bindingSource.DataSource = customers.ToBindingList();
  10. }

However, as this query will be executed on the data in memory, it is really useful only for small tables that do not need to be filtered beforehand.

For a LINQ query to be converted into SQL and executed on the server, we need to access the dbContext.CUSTOMERS directly instead of accessing the dbContext.CUSTOMERS.Local property in the LINQ query. The prior call to dbContext.CUSTOMERS.Load(); to load the collection to memory is not required.

IQueryable and BindingList

IQueryable objects present a small problem: they cannot return BindingList. BindingList is a base class for creating a two-way data-binding mechanism. We can use the IQueryable interface to get a regular list by calling ToList but, this way, we lose handy features such as sorting in the grid and several more. The deficiency was fixed in .NET Framework 5 by creating a special extension. To do the same thing in FW4, we will create our own solution.

  1. public static class DbExtensions
  2. {
  3. // Internal class for map generator values to it
  4. private class IdResult
  5. {
  6. public int Id { get; set; }
  7. }
  8. // Cast IQueryable to BindingList
  9. public static BindingList<T> ToBindingList<T>
  10. (this IQueryable<T> source) where T : class
  11. {
  12. return (new ObservableCollection<T>(source)).ToBindingList();
  13. }
  14. // Get the next value of the sequence
  15. public static int NextValueFor(this DbModel dbContext, string genName)
  16. {
  17. string sql = String.Format(
  18. "SELECT NEXT VALUE FOR {0} AS Id FROM RDB$DATABASE", genName);
  19. return dbContext.Database.SqlQuery<IdResult>(sql).First().Id;
  20. }
  21. // Disconnect all objects from the DbSet collection from the context
  22. // Useful for updating the cache
  23. public static void DetachAll<T>(this DbModel dbContext, DbSet<T> dbSet)
  24. where T : class
  25. {
  26. foreach (var obj in dbSet.Local.ToList())
  27. {
  28. dbContext.Entry(obj).State = EntityState.Detached;
  29. }
  30. }
  31. // Update all changed objects in the collection
  32. public static void Refresh(this DbModel dbContext, RefreshMode mode,
  33. IEnumerable collection)
  34. {
  35. var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
  36. objectContext.Refresh(mode, collection);
  37. }
  38. // Update the object
  39. public static void Refresh(this DbModel dbContext, RefreshMode mode,
  40. object entity)
  41. {
  42. var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
  43. objectContext.Refresh(mode, entity);
  44. }
  45. }

Other Extensions

There are several more extensions in the iQueryable interface:

NextValueFor

is used to get the next value from the generator.

dbContext.Database.SqlQuery

allows SQL queries to be executed directly and their results to be displayed on some entity (projection).

DetachAll

is used to detach all objects of the DBSet collection from the context. It is necessary to update the internal cache, because all retrieved data are cached and are not retrieved from the database again. However, that is not always useful because it makes it more difficult to get the latest version of records that were modified in another context.

In web applications, a context usually exists for a very short period. A new context has an empty cache.

Refresh

is used to update the properties of an entity object. It is useful for updating the properties of an object after it has been edited or added.

Code for Loading the Data

Our code for loading data will look like this:

  1. private void LoadCustomersData()
  2. {
  3. var dbContext = AppVariables.getDbContext();
  4. // disconnect all loaded objects
  5. // this is necessary to update the internal cache
  6. // for the second and subsequent calls of this method
  7. dbContext.DetachAll(dbContext.CUSTOMERS);
  8. var customers =
  9. from customer in dbContext.CUSTOMERS
  10. orderby customer.NAME
  11. select customer;
  12. bindingSource.DataSource = customers.ToBindingList();
  13. }
  14. private void CustomerForm_Load(object sender, EventArgs e)
  15. {
  16. LoadCustomersData();
  17. dataGridView.DataSource = bindingSource;
  18. dataGridView.Columns["INVOICES"].Visible = false;
  19. dataGridView.Columns["CUSTOMER_ID"].Visible = false;
  20. dataGridView.Columns["NAME"].HeaderText = "Name";
  21. dataGridView.Columns["ADDRESS"].HeaderText = "Address";
  22. dataGridView.Columns["ZIPCODE"].HeaderText = "ZipCode";
  23. dataGridView.Columns["PHONE"].HeaderText = "Phone";
  24. }
Adding a Customer

This is the code of the event handler for clicking the Add button:

  1. private void btnAdd_Click(object sender, EventArgs e) {
  2. var dbContext = AppVariables.getDbContext();
  3. // creating a new entity instance
  4. var customer = (CUSTOMER)bindingSource.AddNew();
  5. // create an editing form
  6. using (CustomerEditorForm editor = new CustomerEditorForm()) {
  7. editor.Text = "Add customer";
  8. editor.Customer = customer;
  9. // Form Close Handler
  10. editor.FormClosing += delegate (object fSender,
  11. FormClosingEventArgs fe) {
  12. if (editor.DialogResult == DialogResult.OK) {
  13. try {
  14. // get next sequence value
  15. // and assign it
  16. customer.CUSTOMER_ID = dbContext.NextValueFor("GEN_CUSTOMER_ID");
  17. // add a new customer
  18. dbContext.CUSTOMERS.Add(customer);
  19. // trying to save the changes
  20. dbContext.SaveChanges();
  21. // and update the current record
  22. dbContext.Refresh(RefreshMode.StoreWins, customer);
  23. }
  24. catch (Exception ex) {
  25. // display error
  26. MessageBox.Show(ex.Message, "Error");
  27. // Do not close the form to correct the error
  28. fe.Cancel = true;
  29. }
  30. }
  31. else
  32. bindingSource.CancelEdit();
  33. };
  34. // show the modal form
  35. editor.ShowDialog(this);
  36. }
  37. }

While adding the new record, we used the generator to get the value of the next identifier. We could have done it without applying the value of the identifier, leaving the BEFORE INSERT trigger to fetch the next value of the generator and apply it. However, that would leave us unable to update the added record.

Editing a Customer

The code of the event handler for clicking the Edit button is as follows:

  1. private void btnEdit_Click(object sender, EventArgs e) {
  2. var dbContext = AppVariables.getDbContext();
  3. // get instance
  4. var customer = (CUSTOMER)bindingSource.Current;
  5. // create an editing form
  6. using (CustomerEditorForm editor = new CustomerEditorForm()) {
  7. editor.Text = "Edit customer";
  8. editor.Customer = customer;
  9. // Form Close Handler
  10. editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) {
  11. if (editor.DialogResult == DialogResult.OK) {
  12. try {
  13. // trying to save the changes
  14. dbContext.SaveChanges();
  15. dbContext.Refresh(RefreshMode.StoreWins, customer);
  16. // update all related controls
  17. bindingSource.ResetCurrentItem();
  18. }
  19. catch (Exception ex) {
  20. // display error
  21. MessageBox.Show(ex.Message, "Error");
  22. // Do not close the form to correct the error
  23. fe.Cancel = true;
  24. }
  25. }
  26. else
  27. bindingSource.CancelEdit();
  28. };
  29. // show the modal form
  30. editor.ShowDialog(this);
  31. }
  32. }

The form for editing the customer looks like this:

fbdevgd30 efw 015 en

Figure 25. Customer edit form

The code for binding to data is very simple.

  1. public CUSTOMER Customer { get; set; }
  2. private void CustomerEditorForm_Load(object sender, EventArgs e)
  3. {
  4. edtName.DataBindings.Add("Text", this.Customer, "NAME");
  5. edtAddress.DataBindings.Add("Text", this.Customer, "ADDRESS");
  6. edtZipCode.DataBindings.Add("Text", this.Customer, "ZIPCODE");
  7. edtPhone.DataBindings.Add("Text", this.Customer, "PHONE");
  8. }
Deleting a Customer

The code of the event handler for clicking the Delete button is as follows:

  1. private void btnDelete_Click(object sender, EventArgs e) {
  2. var dbContext = AppVariables.getDbContext();
  3. var result = MessageBox.Show("Are you sure you want to delete the customer?",
  4. "Confirmation",
  5. MessageBoxButtons.YesNo,
  6. MessageBoxIcon.Question);
  7. if (result == DialogResult.Yes) {
  8. // get the entity
  9. var customer = (CUSTOMER)bindingSource.Current;
  10. try {
  11. dbContext.CUSTOMERS.Remove(customer);
  12. // trying to save the changes
  13. dbContext.SaveChanges();
  14. // remove from the linked list
  15. bindingSource.RemoveCurrent();
  16. }
  17. catch (Exception ex) {
  18. // display error
  19. MessageBox.Show(ex.Message, "Error");
  20. }
  21. }
  22. }