Windows DataGridView with inline edit and remove buttons
Introduction
There are several methods to permit editing and removal of records displayed in a Windows DataGridView from inline editing of cells to display a modal window invoked by either a button or by double-clicking the current DataGridView row/cells or a combination of both. An alternate approach is to provide buttons within the DataGridView, one for edit and one for removal of a row. This article will explain how to implement editing and deleting the current row in a DataGridView utilizing a custom DataGridView button column which is set up for displaying the buttons on the current row only rather than displaying the buttons on all rows at once.
Screenshots
The following screenshot depicts the final product.
This screenshot depicts a user performing a request to remove the current row which will prompt to confirm the delete current row
Editing the current record
Removing records considerations
There are two things to consider when a user wants to remove a record. First off if there is no prompt to remove the record and this was done accidentally there is no undoing the delete operation unless there are backups made that can be used to restore the record, your application records to an alternate database all changes made to the production database tables or you prompt them to confirm the record removal. Secondly, when prompting for removal you should always default the prompt to a negative response which in the case in the screenshot above, No is the default response. This means a user is hard-pressed to say “I did this by mistake”.
Special note on removal of records. We must consider the user uses the row-header to remove a record. In the source code provided, note the code for this consideration.
We subscribe to UserDeletingRow in form Shown event.
dataGridView1.UserDeletingRow += formData.UserDeletingRow;
Our form name is Form1, note the event is in formData which is a class used to remove several logical parts from Form1. It's advisable to detach as much code as possible from a form.
Here is our event for capturing a user performing a delete operation by selecting a row in the DataGridView and pressing the delete key. KarenDialog is a static class with a wrapper method for MessageBox.Show. We can expand on this but that is outside of what we are here to learn about.
public void UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e)
{
if (KarenDialogs.Question($"Remove '{BindingSource.CurrentRow().Field<string>("CompanyName")}'"))
{
if (Operations.FakeRemoveCustomer(BindingSource.CurrentRow().Field<int>("CustomerIdentifier")))
{
BindingSource.RemoveCurrent();
}
}
else
{
e.Cancel = true;
}
}
Edit current record considerations
When editing the current record if there are any fields/columns that have constraints e.g. using the edit screenshot above, contact title must be one in a list so we provide a combo box to display valid items. In this case, there is a column for country which violates this rule and has been done so to compare the two, one which constrains and one which does not. What do you think will happen if the user enters Mexeco when they intended to use Mexico? Any reports that look for customers from Mexico would not include records that misspelled Mexico.
Code for the editor form
using SqlDataOperations;
using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
namespace DataGridViewButtonExample
{
/// <summary>
/// There are several different ways to handle pushing and popping
/// data to this form, this is simply one of them that is easy
/// to follow.
///
/// There is no validation e.g. is a field empty or perhaps
/// in your app there may be constraints e.g. no duplicates so
/// that would be out of scope of this code sample which focuses
/// on the DataGridViewButton.
/// </summary>
public partial class EditorForm : Form
{
private DataRow _Row;
public DataRow Row => _Row;
public EditorForm()
{
InitializeComponent();
}
public EditorForm(DataRow pRow, List<ContactType> contactList)
{
InitializeComponent();
_Row = pRow;
cboContactTitle.DataSource = contactList;
cboContactTitle.SelectedIndex = cboContactTitle
.FindString(_Row.Field<string>("ContactTitle"));
txtCompanyName.Text = Row.Field<string>("CompanyName");
txtContactName.Text = Row.Field<string>("ContactName");
txtCity.Text = Row.Field<string>("City");
txtCountry.Text = Row.Field<string>("Country");
}
/// <summary>
/// No validation done
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveButton_Click(object sender, EventArgs e)
{
_Row.SetField<string>("CompanyName", txtCompanyName.Text);
_Row.SetField<string>("ContactName", txtContactName.Text);
_Row.SetField<string>("ContactTitle", cboContactTitle.Text);
_Row.SetField<int>("ContactTypeIdentifier",
((ContactType)cboContactTitle.SelectedItem).ContactTypeIdentifier);
_Row.SetField<string>("City", txtCity.Text);
_Row.SetField<string>("Country", txtCountry.Text);
}
}
}
The following method in a separate data class retrieves contact titles from a SQL-Server database table.
public List<ContactType> LoadContactTypes()
{
mHasException = false;
var contactList = new List<ContactType>();
using (SqlConnection cn = new SqlConnection() { ConnectionString = ConnectionString })
{
using (SqlCommand cmd = new SqlCommand() { Connection = cn })
{
cmd.CommandText = "SELECT ContactTypeIdentifier,ContactTitle FROM dbo.ContactType";
try
{
cn.Open();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
contactList.Add(new ContactType()
{
ContactTypeIdentifier = reader.GetInt32(0),
ContactTitle = reader.GetString(1)
});
}
}
catch (Exception ex)
{
mHasException = true;
mLastException = ex;
}
}
}
return contactList;
}
Note that the data class inherits a base class for configuring the connection to the SQL-Server database and this base class inherits a base exception class which permits implementing a repeatable pattern for capturing exceptions and not to throw the exceptions in the data class but instead permit the caller to deal with the exception without the need for wrapping a data operation in a try/catch.
Custom DataGridView button
The custom column is in a class project.
To create a custom button there is a language extension method in the windows form project.
public static void CreateUnboundButtonColumn(
this DataGridView pDataGridView,
string pColumnName,string pColumnText,
string pHeaderText,
int pWith = 60)
{
DataGridViewDisableButtonColumn Column = new DataGridViewDisableButtonColumn
{
HeaderText = pHeaderText,
Name = pColumnName,
Text = pColumnText,
Width = pWith,
UseColumnTextForButtonValue = true
};
pDataGridView.Columns.Insert(pDataGridView.ColumnCount, Column);
}
There is a good deal of interaction between the button which means their names need to be available for various events.
private string removeButtonName = "RemoveColumn";
private string editButtonName = "EditColumn";
There is also a need for setting up the text to display in the buttons
formData.RemoveButtonName = removeButtonName;
formData.EditButtonName = editButtonName;
Once the columns are created we need to perform adjustments so only the current row buttons are visible.
dataGridView1.AdjustButtons(removeButtonName);
dataGridView1.AdjustButtons(editButtonName);
public static void AdjustButtons(this DataGridView pDataGridView, string pCellName = "Details")
{
for (int i = 0; i < pDataGridView.RowCount; i++)
{
pDataGridView.Rows[i].Cells[pCellName].Value = pCellName;
((DataGridViewDisableButtonCell)(pDataGridView.Rows[i].Cells[pCellName])).Enabled = false;
}
((DataGridViewDisableButtonCell)(pDataGridView.Rows[pDataGridView.CurrentRow.Index]
.Cells[pCellName])).Enabled = true;
}
The above handles sorting columns and initial setup while the following shows/hides the buttons while traversing DataGridView rows.
public void CellEnter(object sender, DataGridViewCellEventArgs e)
{
var dgv = ((DataGridView)sender);
DataGridViewDisableButtonCell removeButtonCell = (DataGridViewDisableButtonCell)
(dgv.Rows[e.RowIndex].Cells[RemoveButtonName]);
DataGridViewDisableButtonCell editButtonCell = (DataGridViewDisableButtonCell)
(dgv.Rows[e.RowIndex].Cells[EditButtonName]);
removeButtonCell.Enabled = true;
editButtonCell.Enabled = true;
if (removeButtonCell.Value == null)
{
removeButtonCell.Value = RemoveButtonName;
}
if (editButtonCell.Value == null)
{
editButtonCell.Value = EditButtonName;
}
dgv.Invalidate();
}
public void CellLeave(object sender, DataGridViewCellEventArgs e)
{
var dgv = ((DataGridView)sender);
DataGridViewDisableButtonCell removeButtonCell = (DataGridViewDisableButtonCell)
(dgv.Rows[e.RowIndex].Cells[RemoveButtonName]);
DataGridViewDisableButtonCell editButtonCell = (DataGridViewDisableButtonCell)
(dgv.Rows[e.RowIndex].Cells[EditButtonName]);
removeButtonCell.Enabled = false;
editButtonCell.Enabled = false;
dgv.Invalidate();
}
Data operations
All data is retrieved from a SQL-Server database using a class within a class project dedicated to working with this data. The edit and removal methods "do nothing", are stub methods.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using TeamBaseLibrary;
namespace SqlDataOperations
{
/// <summary>
/// BaseSqlServerConnections handles connections which can be overridden
/// along with implementing BaseExeptionhandler for implementing default
/// exception handling in try/catch statements
/// </summary>
public class Operations : BaseSqlServerConnections
{
public Operations()
{
DefaultCatalog = "NorthWindAzure";
}
public DataTable LoadCustomerData(bool pHidePrimaryKey = true)
{
mHasException = false;
var dt = new DataTable();
using (SqlConnection cn = new SqlConnection() { ConnectionString = ConnectionString })
{
using (SqlCommand cmd = new SqlCommand() { Connection = cn })
{
try
{
cmd.CommandText =
"SELECT C.CustomerIdentifier, C.CompanyName, C.ContactName, ContactType.ContactTitle, " +
"C.City, C.Country, C.ContactTypeIdentifier " +
"FROM dbo.Customers AS C " +
"INNER JOIN ContactType ON C.ContactTypeIdentifier = ContactType.ContactTypeIdentifier;";
cn.Open();
dt.Load(cmd.ExecuteReader());
dt.Columns["ContactTypeIdentifier"].ColumnMapping = MappingType.Hidden;
if (pHidePrimaryKey)
{
dt.Columns["CustomerIdentifier"].ColumnMapping = MappingType.Hidden;
}
}
catch (Exception ex)
{
mHasException = true;
mLastException = ex;
}
}
}
return dt;
}
public IEnumerable<Customer> GetCustomersLimitedViaIterator()
{
mHasException = false;
using (SqlConnection cn = new SqlConnection() { ConnectionString = ConnectionString })
{
using (SqlCommand cmd = new SqlCommand() { Connection = cn })
{
cmd.CommandText = "SELECT CustomerIdentifier, CompanyName, ContactName FROM dbo.Customers";
SqlDataReader reader = null;
try
{
cn.Open();
reader = cmd.ExecuteReader();
}
catch (Exception ex)
{
mHasException = true;
mLastException = ex;
}
while (reader.Read())
{
yield return new Customer()
{
CustomerIdentifier = reader.GetInt32(0),
CompanyName = reader.GetString(1),
ContactName = reader.GetString(2)
};
}
}
}
}
public List<ContactType> LoadContactTypes()
{
mHasException = false;
var contactList = new List<ContactType>();
using (SqlConnection cn = new SqlConnection() { ConnectionString = ConnectionString })
{
using (SqlCommand cmd = new SqlCommand() { Connection = cn })
{
cmd.CommandText = "SELECT ContactTypeIdentifier,ContactTitle FROM dbo.ContactType";
try
{
cn.Open();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
contactList.Add(new ContactType()
{
ContactTypeIdentifier = reader.GetInt32(0),
ContactTitle = reader.GetString(1)
});
}
}
catch (Exception ex)
{
mHasException = true;
mLastException = ex;
}
}
}
return contactList;
}
#region Stub methods for remove and update
/// <summary>
/// Here is where you would have a DELETE statement to remove
/// the record by primary key
/// </summary>
/// <param name="pIdentifier"></param>
/// <returns></returns>
public bool FakeRemoveCustomer(int pIdentifier)
{
return true;
}
/// <summary>
/// Here is where you would update the customer by the
/// primary key in the DataRow followed by using field
/// values to use in the SET part of the UPDATE statement.
/// </summary>
/// <param name="pRow"></param>
/// <returns></returns>
public bool FakeUpdateCustomer(DataRow pRow)
{
return true;
}
#endregion
}
}
Language extension methods
These extension methods permit code to be method based and reusable. The first cast our BindingSource component which it's DataSource is set to a DataTable and the BindingSource becomes the DataSource of the DataGridView. In turn for this code sample, the DataColumn names are the names for the DataGridView columns.
using System.Data;
using System.Windows.Forms;
namespace TeamBaseLibrary
{
public static class BindingSourceExtensions
{
public static DataTable DataTable(this BindingSource pBindingSource)
{
return (DataTable)pBindingSource.DataSource;
}
public static DataRow CurrentRow(this BindingSource pBindingSource)
{
return ((DataRowView)pBindingSource.Current).Row;
}
}
}
The following extension method is a helper method which as in the above example allows repeatable usage and method base design.
namespace TeamBaseLibrary.Extensions
{
public static class GenericExtensions
{
/// <summary>
/// Determine if object is a specific type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="pObject"></param>
/// <returns></returns>
public static bool IsA<T>(this object pObject)
{
return pObject is T;
}
}
}
Requirements
- Visual Studio 2017 (should work in VS2015)
- SQL-Server (at least version 2012 or higher)
- Execute the data scripts in the MSDN code sample.
Full source code
Please visit the following MSDN code sample.
Ending notes
In closing, this is not a drop into your project and use solution for adding a DataGridViewButton or more to your DataGridView but instead must study the code presented to understand and then once this is done you can utilize this in your project. The only other way is to purchase a third party DataGridView that offers buttons only shown per current row.
Important Original source code was all synchronous data reads. In one case the current code does one asynchronous call to the database. Original source code was on MSDN code site which does not exists now. The source code has been moved to https://github.com/karenpayneoregon/DataGridViewButtonAsync |