다음을 통해 공유


DataGridView setup for surveys

Introduction

The DataGridView control is very flexible were they are easy to setup and use yet without proper knowledge of this control along with understanding user interface design a working solution can be completed but may not make sense to the users of the application.

This article will discuss proper usage for utilizing a Windows Form DataGridView control for asking several questions that are mutually inclusive where only one selection can be made per question/row.

Basics usage of a DataGridViewCheckBoxColumn

Before diving into how to create a DataGridView appropriate for asking opinions in a fixed survey style let’s look at how a basic DataGridView might be setup with a DataGridViewCheckBoxColumn.


In this example shown above the DataGridView DataSource is set to a DataTable with mocked data, all examples which follow use the same mocked data.

using System.Collections.Generic;
using System.Data;
 
namespace MockedDataLibrary
{
    /// <summary>
    /// Simple mockup of data that may come from a database table.
    /// </summary>
    public class  MockData
    {
        private readonly  DataTable _customerTable;
        public DataTable Table { get { return _customerTable; } }
        public MockData()
        {
            var companyNames = new  List<string>()
            {
                "Alfreds Futterkiste",
                "Ana Trujillo Emparedados y helados",
                "Around the Horn",
                "Bólido Comidas preparadas",
                "Folies gourmandes",
                "Königlich Essen",
                "QUICK Stop",
                "Toms Spezialitäten"
            };
            _customerTable = new  DataTable();
 
            _customerTable.Columns.Add(new DataColumn()
            {
                ColumnName = "CustomerIdentifier",
                DataType = typeof(int),
                AutoIncrement = true, AutoIncrementSeed = 1
            });
 
            _customerTable.Columns.Add(new DataColumn()
            {
                ColumnName = "CompanyName", DataType = typeof(string)
            });
             
 
            companyNames.ForEach(name => _customerTable.Rows.Add(null, name));
 
        }
    }
}

In the Shown event of the form, the DataGridView is setup with the mocked data, a DataColumn is inserted for the DataGridViewCheckBoxColumn and several properties are set for the DataGridView. Note that the DataGridView.DataSource is set to a BindingSource component with is not required yet the BindingSource offers valuable features like moving the current row up, down, first or last along with other useful functionality.

In the button click event the code is responsible for obtaining rows in the DataGridView that have their check box value set to true.

using System;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using MockedDataLibrary;
 
namespace DataGridViewCheckBoxConventional
{
    public partial  class Form1 : Form
    {
        private BindingSource bsCustomers = new BindingSource();
        public Form1()
        {
            InitializeComponent();
            Shown += Form1_Shown;
        }
 
        private void  Form1_Shown(object  sender, EventArgs e)
        {
            var ops = new  MockData();
            var table = ops.Table;
 
            table.Columns.Add(new DataColumn()
            {
                ColumnName = "Process",
                DataType = typeof(bool),
                DefaultValue = false
            });
 
            table.Columns["Process"].SetOrdinal(0);
            table.Columns["CustomerIdentifier"].ColumnMapping = MappingType.Hidden;
             
            bsCustomers.DataSource = table;
            dataGridView1.AllowUserToAddRows = false;
            dataGridView1.DataSource = bsCustomers;
            dataGridView1.Columns["CompanyName"].HeaderText = "Company";
            dataGridView1.Columns["CompanyName"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
        }
 
        private void  processButton_Click(object sender, EventArgs e)
        {
            var results = ((DataTable) bsCustomers.DataSource)
                .AsEnumerable()
                .Where(row => row.Field<bool>("Process"))
                .Select(row => new  Company()
                {
                    id = row.Field<int>("CustomerIdentifier"),
                    Name = row.Field<string>("CompanyName")
                }).ToList();
 
            if (results.Count >0)
            {
                var sb = new  StringBuilder();
                foreach (Company company in results)
                {
                    sb.AppendLine(company.ToString());
                }
 
                MessageBox.Show(sb.ToString());
            }
            else
            {
                MessageBox.Show("Please select one or more companies.");
            }
        }
    }
}

First attempt for a survey

In this example DataGridViewCheckBox columns are used where there is logic to allow only one selection per DataGridViewRow.

The code is fairly easy to follow. In the Form Shown event, data is loaded as done in the prior code sample, events are setup to handle only allowing one selection per row. In the CurrentCellDirtyStateChanged for the DataGridView this fires when a selection is made which commits the change which works in tangent with CellValueChanged event which sets the current DataGridViewCheckBoxCell to true and any others to false. And note column names are not hard coded, they are obtained dynamically at run time.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using DataGridViewMultiCheckBoxes.Extensions;
using MockedDataLibrary;
 
namespace DataGridViewMultiCheckBoxes
{
     
    public partial  class Form1 : Form
    {
        private BindingSource bsCustomers = new BindingSource();
        private List<string> _checkBoxColumnNames = new  List<string>();
        public Form1()
        {
            InitializeComponent();
            Shown += Form1_Shown;
        }
 
        private void  Form1_Shown(object  sender, EventArgs e)
        {
            var ops = new  MockData();
 
            bsCustomers.DataSource = ops.Table;
 
            dataGridView1.AutoGenerateColumns = false;
 
            // assign customers to the datagridview
            dataGridView1.DataSource = bsCustomers;
 
            dataGridView1.CurrentCellDirtyStateChanged += DataGridView1_CurrentCellDirtyStateChanged;
            dataGridView1.CellValueChanged += DataGridView1_CellValueChanged;
 
            _checkBoxColumnNames = dataGridView1.Columns.OfType<DataGridViewCheckBoxColumn>()
                .Select(col => col.Name)
                .ToList();
 
        }
 
        private void  DataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
        {
            // assert we have a valid row
            if (dataGridView1.CurrentRow == null) return;
 
            // we only want to work with checkbox cells
            if (!(dataGridView1.CurrentCell is DataGridViewCheckBoxCell)) return;
 
            // there are three checkbox cells, get the others ones other then the current cell
            var others = _checkBoxColumnNames
                .Where(name => name != dataGridView1.Columns[dataGridView1.CurrentCellValue().ColumnIndex].Name)
                .ToArray();
 
            // ensure only one is selected
            if ((bool)dataGridView1.CurrentCell.Value)
            {
                foreach (var colName in others)
                {
                    dataGridView1.CurrentRow.Cells[colName].Value = false;
                }
            }
        }
        /// <summary>
        /// Commit change
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void  DataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e)
        {
            if (dataGridView1.CurrentCell is DataGridViewCheckBoxCell)
            {
                dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
            }
        }
 
        private void  cmdReset_Click(object sender, EventArgs e)
        {
            for (var index = 0; index < dataGridView1.Rows.Count; index++)
            {
                foreach (var columnName in _checkBoxColumnNames)
                {
                    dataGridView1.Rows[index].Cells[columnName].Value = false;
                }
            }
        }
 
        private void  cmdGetSelection_Click(object sender, EventArgs e)
        {
            var ratings = new  List<SelectedItem>();
            var notRated = new  StringBuilder();
 
            foreach (DataGridViewRow row in dataGridView1.Rows)
            {
                if (row.Cells.OfType<DataGridViewCheckBoxCell>().Any(t => t.Value != null))
                {
                    var item = new  SelectedItem
                    {
                        CompanyName = row.Cells["CompanyNameColumn"].Value.ToString(),
                        Rating = row.Cells.OfType<DataGridViewCheckBoxCell>()
                            .FirstOrDefault(cell => (bool) cell.Value).OwningColumn.HeaderText
                    };
 
                    ratings.Add(item);
                }
                else
                {
                    notRated.AppendLine(row.Cells["CompanyNameColumn"].Value.ToString());
                }
            }
             
            if (notRated.Length >0)
            {
                MessageBox.Show($"Please rate these\n\n{notRated}");
            }
        }
    }
}

Although this works a savvy user may be confused at first as checkbox inputs are usually used to permit more than one selection such a a radio button.  This is where a developer should realize that a checkbox column is not proper for this task. 

Solution

Radio buttons will be best for this task yet there is no standard DataGridViewRadioButton column.  The simple solution with minor drawbacks is to use a DataGridViewImageColumn with two images, one for selected and the other for unselected.

A custom column might suit some as provided here from Microsoft.
Another possible solution is found here but needs work. Here is the author's version of it in VB.NET.
The last option is using a third party grid.

In the screenshot below three columns have been setup with image columns.

There are several task which are required such as clearing a row's selections before selecting a cell. In the language extension class below there is an extension method for clearing a row which is used in an overloaded extension method to set the desired cell.

The overloaded method is to permit identifying a row by row index or column name.

using DataGridViewCheckBoxHelpers;
using System.Linq;
using System.Windows.Forms;
 
namespace DataGridViewFalseRadioButton
{
    public static  class DataGridViewSelectionExtensions
    {
        /// <summary>
        /// Unselect all DataGridViewImageCells. If pRow is out of range an exception is thrown.
        /// </summary>
        /// <param name="pDataGridView"></param>
        /// <param name="pRow">Row index to clear</param>
        public static  void UnSelect(this DataGridView pDataGridView, int pRow)
        {
            pDataGridView.Rows[pRow].Cells.OfType<DataGridViewImageCell>().ToList().ForEach(col =>
                {
                    col.Tag = ImageSelection.Unselected;
                    col.Value = Properties.Resources.RadioButtonUnselected;
                }
            );
        }
        /// <summary>
        /// Select option by row index, column name
        /// </summary>
        /// <param name="pDataGridView"></param>
        /// <param name="pRow">Row index in pDataGridView</param>
        /// <param name="pColumnName">Column name in pDataGridView</param>
        public static  void SetSelection(this DataGridView pDataGridView, int pRow, string pColumnName)
        {
            if (!pDataGridView.Columns.Contains(pColumnName))  return;
 
            pDataGridView.UnSelect(pRow);
 
            pDataGridView.Rows[pRow].Cells[pColumnName].Tag = ImageSelection.Selected;
            pDataGridView.Rows[pRow].Cells[pColumnName].Value = Properties.Resources.RadioButtonSelected;
 
        }
        /// <summary>
        /// Select option by row index, column index
        /// </summary>
        /// <param name="pDataGridView"></param>
        /// <param name="pRow">Row index in pDataGridView</param>
        /// <param name="pColumnIndex">Column index in pDataGridView</param>
        public static  void SetSelection(this DataGridView pDataGridView, int pRow, int pColumnIndex)
        {
            if (!pDataGridView.Columns.Contains(pDataGridView.Columns[pColumnIndex].Name)) return;
 
 
            pDataGridView.UnSelect(pRow);
 
            pDataGridView.Rows[pRow].Cells[pColumnIndex].Tag = ImageSelection.Selected;
            pDataGridView.Rows[pRow].Cells[pColumnIndex].Value = Properties.Resources.RadioButtonSelected;
 
        }
 
    }
}

Note the following lines extracted from code above. These point to images used for selection/unselect a DataGridViewImageCell value

pDataGridView.Rows[pRow].Cells[pColumnIndex].Tag = ImageSelection.Selected;
pDataGridView.Rows[pRow].Cells[pColumnName].Tag = ImageSelection.Selected;

ImageSelection is an enumeration used with the code above which permits querying the current DataGridViewImageCell state, selected or unselected.

public enum  ImageSelection
{
    Unselected = 1,
    Selected = 2
}

The following class is used to strongly type information for the DataGridViewImageColumn for determination of choices along with passing the results to a method to process the results although in the code sample presented they are displayed in a child window.

namespace DataGridViewCheckBoxHelpers
{
    public class  SelectedRadioButton
    {
        /// <summary>
        /// DataGridView Row index
        /// </summary>
        public int  RowIndex { get; set; }
        /// <summary>
        /// Column name in DataGridView
        /// </summary>
        public string  ColumnName { get; set; }
        /// <summary>
        /// Column index in DataGridView column collection
        /// </summary>
        public int  ColumnIndex { get; set; }
        /// <summary>
        /// Rating for a row
        /// </summary>
        public Ratings Rating { get; set; }
        /// <summary>
        /// Primary key from data source
        /// </summary>
        public int  id { get; set; }
        /// <summary>
        /// Company name we are rating
        /// </summary>
        public string  CompanyName { get; set; }
        public override  string ToString()
        {
            return CompanyName;
        }
    }
}

Form code
In the Shown event a BindingSource component is a assigned a DataTable containing text to display for user responses, company names as in the prior examples. In turn the BindingSource becomes the DataSource of the DataGridView which has predefined columns, one for company name a DataGridViewTextBoxColumn, three for selections which are DataGridViewImage columns.

Once the DataSource for the DataGridView has been set each DataGridViewImageCell are setup as unselected. Sorting for the DataGridView is turned off to keep company names in order, this is optional.

The last three lines in Form Shown event show how to manually set a selection. First row 1 has the first DataGridViewImageCell selected followed by the third which clears the first one then selects the middle one which clears the first and third.

private void  Form1_Shown(object  sender, EventArgs e)
{
    var ops = new  MockData();
 
    _bsCustomers.DataSource = ops.Table;
 
    dataGridView1.AutoGenerateColumns = false;
    dataGridView1.DataSource = _bsCustomers;
    dataGridView1.Columns.OfType<DataGridViewImageColumn>().Select(col => col)
        .ToList()
        .ForEach(col => col.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleCenter
    );
 
    for (var rowIndex = 0; rowIndex < dataGridView1.Rows.Count; rowIndex++)
    {
        dataGridView1.UnSelect(rowIndex);
    }
 
 
    dataGridView1.Columns["CompanyNameColumn"].SortMode = DataGridViewColumnSortMode.NotSortable;
 
    /*
        * Selection row 0 selection three time, this shows the language extension functions as expected.
        * (Each DataGridView column were created in the designer)
        */
    dataGridView1.SetSelection(0, "Option1Column");
    dataGridView1.SetSelection(0, "Option3Column");
    dataGridView1.SetSelection(0, "Option2Column");
 
}

CellClick event of the DataGridView handles clearing and making a selection via a mouse click and setting the current row's error text to an empty string. Note the error text is set in a button click event which will come up next where if a row has no selection it's marked for attention.

private void  DataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
    if (!(dataGridView1.CurrentCell is DataGridViewImageCell)) return;
 
    if ((int)dataGridView1.CurrentCell.Tag == (int)ImageSelection.Selected)
    {
        dataGridView1.CurrentCell.Value = Properties.Resources.RadioButtonUnselected;
        dataGridView1.CurrentCell.Tag = ImageSelection.Unselected;
    }
    else if  ((int)dataGridView1.CurrentCell.Tag == (int)ImageSelection.Unselected)
    {
        if (dataGridView1.CurrentRow != null)
        {
            var imageCells = dataGridView1.CurrentRow.Cells.OfType<DataGridViewImageCell>()
                .Select(col => col);
 
            foreach (var item in imageCells)
            {
                item.Value = Properties.Resources.RadioButtonUnselected;
                item.Tag = ImageSelection.Unselected;
 
            }
        }
 
        dataGridView1.CurrentCell.Tag = ImageSelection.Selected;
        dataGridView1.CurrentCell.Value = Properties.Resources.RadioButtonSelected;
 
        if (!string.IsNullOrWhiteSpace(dataGridView1.CurrentRow.ErrorText))
        {
            dataGridView1.CurrentRow.ErrorText = "";
        }
    }
}

For obtaining selections via a button click an extension method is used which resides in a class project ready for use in your solution.

using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Windows.Forms;
 
namespace DataGridViewCheckBoxHelpers
{
    public static  class DataGridViewExtensions
    {
        /// <summary>
        /// Retrieves all rows in the DataGridView with a radio selection
        /// </summary>
        /// <param name="pDataGridView"></param>
        /// <param name="pBindingSource"></param>
        /// <param name="pIdentifierColumnName">Primary key</param>
        /// <param name="pFieldName">Field name displayed</param>
        /// <returns></returns>
        public static  List<SelectedRadioButton> GetSelections(
            this DataGridView pDataGridView, BindingSource pBindingSource, string  pIdentifierColumnName, string pFieldName)
        {
            var selectedRadioButtonList = new  List<SelectedRadioButton>();
            DataGridViewColumn col;
 
            foreach (DataGridViewRow row in pDataGridView.Rows)
            {
                var imageCell = row.Cells.OfType<DataGridViewImageCell>()
                    .FirstOrDefault(data => (int)data.Tag == (int)ImageSelection.Selected);
 
                if (imageCell == null) continue;
                col = pDataGridView.CurrentRow.Cells[imageCell.ColumnIndex].OwningColumn;
 
                selectedRadioButtonList.Add(new SelectedRadioButton()
                {
                    RowIndex = imageCell.RowIndex,
                    ColumnName = col.Name,
                    ColumnIndex = col.Index,
                    Rating = col.Index.ToEnum<Ratings>(),
                    id = pBindingSource.DataTable().Rows[imageCell.RowIndex].Field<int>(pIdentifierColumnName),
                    CompanyName = pBindingSource.DataTable().Rows[imageCell.RowIndex].Field<string>(pFieldName)
                });
            }
 
            return selectedRadioButtonList;
 
        }
        /// <summary>
        /// Returns a list of DataGridView row indices that don't have a radio selection
        /// </summary>
        /// <param name="pDataGridView"></param>
        /// <param name="pBindingSource"></param>
        /// <returns></returns>
        public static  List<int> GetNoneSelections(this DataGridView pDataGridView, BindingSource pBindingSource)
        {
            var rowsIndices = new  List<int>();
 
            foreach (DataGridViewRow row in pDataGridView.Rows)
            {
                if (!row.Cells.OfType<DataGridViewImageCell>()
                    .Any(data => (int)data.Tag == (int)ImageSelection.Selected))
                {
                    rowsIndices.Add(row.Index);
                }
            }
 
            return rowsIndices;
 
        }
    }
}

Back to obtaining selections in the button click event. If not all rows have a selection the user is presented with error icons for each row that does not have a selection or if all are selected a child window displays the selections made. At this point selections may be stored to a database or used on the fly to make a decision or send off to a web service.

private void  cmdGetSelection_Click(object sender, EventArgs e)
{
    for (int indexer = 0; indexer < dataGridView1.Rows.Count; indexer++)
    {
        dataGridView1.Rows[indexer].ErrorText = "";
    }
 
    var selectedRadioButtonList = dataGridView1
        .GetSelections(_bsCustomers, "CustomerIdentifier","CompanyName");
 
    if (selectedRadioButtonList.Count != _bsCustomers.Count)
    {
 
        var notSelectedIndices = dataGridView1.GetNoneSelections(_bsCustomers);
        for (int indexer = 0; indexer < notSelectedIndices.Count; indexer++)
        {
            dataGridView1.Rows[notSelectedIndices[indexer]].ErrorText = "Must provide a rating";
        }
 
        MessageBox.Show("Please provide a rating for each company.");
        return;
    }
 
    if (selectedRadioButtonList.Count > 0)
    {
 
        var f = new  ResultsForm(selectedRadioButtonList);
 
        try
        {
            f.ShowDialog();
        }
        finally
        {
            f.Dispose();
        }
    }           
}

How to work this into your projects

  • Traverse through DataGridViewFalseRadioButton
    • Study the code.
    • Set breakpoints in the code and step through inspecting values.
    • Consider if the current flow works for you or needs code changes before implementing in your project.
    • Does the author's code style work with your code style? If not this is the time to change it to what you prefer.
  • Copy the images from the above project's resource folder into your resources in your project.
  • Create a DataGridView with a DataGridViewImageColumn for each choice a user can make. In the wild it would be best to provide a neutral choice in the case there is no useful opinion or the user may not know enough to make an informative decision on all rows. 
  • Copy the class project DataGridViewCheckBoxHelpers into your Visual Studio Solution and make sure this library is using the same version of the .NET Framework your current projects in the solution are using.

Summary

This article has presented methods to present users with radio button selections is a standard DataGridView control suitable for surveys or use with setting user preferences in an application along with alternate solutions.

See also

Requires

Microsoft Visual Studio 2015 or higher

Source code

https://github.com/karenpayneoregon/DataGridViewWithRadioButtons