Search code examples
c#data-bindingdatagridviewdatagridviewcolumn

How can I detect when the DataGridView.RowValidating event is from a user interaction vs BindingSource list change?


I have finally tracked down a bug that I've been working on for the entire weekend, however I don't see a way to really solve it - in a clean way, that is.

The situation is that I have a DGV control that is bound to a List of business objects. One of the columns is NOT bound to the DataSource and I have wired up the CellParsing and CellFormatting events to handle the persistence of the data for this cell. I've basically circumvented the .net databinding and implemented my own poor version. This was not intentional, it's REALLY old code that had complicated parsing and formatting requirements and I incorrectly implemented a solution based on an unbound column. I now know the correct way to handle this and have since fixed my code, however I'd still like to know how I could have solved the bug another way.

I handle the RowValidating event and do one final validation on the row as a whole to ensure everything is cool. Of course if there is an issue I Cancel the validation and the row is not committed. This all works fine when the user is editing and adding rows through UI interaction, but creates a problem when the DataSource is set. The issue appears to be that CellFormatting is not called when the DGV updates it's internal list and builds the rows, or at least it's not called before the validating event is fired. This results in the RowValidating handler pulling a null value from the Unbound column (because CellFormatting hasn't been called and set the value yet).

I was refreshing my knowledge on the DGV and thought that handling the CellValueNeeded event may be the ticket but setting DataGridViewCellValueEventArgs.Value didn't trigger the CellFormatting event like I hoped it would.

I've been thinking about how to handle this situation and the only thing I have come up with is to detect when the validation is triggered from a UI event rather than the initial binding or bound list change. Not only is this a hacky solution, but don't see how it could be done.

I've created a complete example application that will illustrate the problem. I'd be really curious to see how some of you would solve a problem like this. It's likely that there is major design smell here.

using System;
using System.Collections.Generic;
using System.Windows.Forms;

public class Form1 : Form
{
    private List<DomainModel> _sampleData;
    public Form1()
    {
        InitializeComponent();

        _sampleData = new List<DomainModel>();
        _sampleData.Add(new DomainModel("Widget A"));
        _sampleData.Add(new DomainModel("Widget B"));
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Setting DataSource");
        domainModelBindingSource.DataSource = _sampleData;
    }

    private void dataGridView1_CellFormatting(object sender,
        DataGridViewCellFormattingEventArgs e)
    {
        Console.WriteLine("CellFormatting fired for {0},{1}",
            e.RowIndex, e.ColumnIndex);

        if (e.ColumnIndex != 0 && !dataGridView1.Rows[e.RowIndex].IsNewRow)
        {
            var model = domainModelBindingSource[e.RowIndex] as DomainModel;
            e.Value = model.Name;
            e.FormattingApplied = true;
        }
    }

    private void dataGridView1_CellParsing(object sender, DataGridViewCellParsingEventArgs e)
    {
        if (e.ColumnIndex == 1)
        {
            e.Value = e.Value.ToString();
            e.ParsingApplied = true;
        }
    }

    private void dataGridView1_RowValidating(object sender, DataGridViewCellCancelEventArgs e)
    {
        if (dataGridView1.Rows[e.RowIndex].IsNewRow)
            return;

        object value = dataGridView1[1, e.RowIndex].Value;
        if (value == null || String.IsNullOrEmpty(value.ToString()))
            e.Cancel = true;
    }

    #region Designer stuff

    private System.ComponentModel.IContainer components = null;
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
            components.Dispose();

        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    private void InitializeComponent()
    {
            this.components = new System.ComponentModel.Container();
            this.dataGridView1 = new System.Windows.Forms.DataGridView();
            this.nameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
            this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
            this.domainModelBindingSource = new System.Windows.Forms.BindingSource(this.components);
            this.button1 = new System.Windows.Forms.Button();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.domainModelBindingSource)).BeginInit();
            this.SuspendLayout();
            // 
            // dataGridView1
            // 
            this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
            | System.Windows.Forms.AnchorStyles.Left) 
            | System.Windows.Forms.AnchorStyles.Right)));
            this.dataGridView1.AutoGenerateColumns = false;
            this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
            this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
            this.nameDataGridViewTextBoxColumn,
            this.Column1});
            this.dataGridView1.DataSource = this.domainModelBindingSource;
            this.dataGridView1.Location = new System.Drawing.Point(12, 41);
            this.dataGridView1.Name = "dataGridView1";
            this.dataGridView1.Size = new System.Drawing.Size(437, 161);
            this.dataGridView1.TabIndex = 0;
            this.dataGridView1.CellFormatting += new System.Windows.Forms.DataGridViewCellFormattingEventHandler(this.dataGridView1_CellFormatting);
            this.dataGridView1.CellParsing += new System.Windows.Forms.DataGridViewCellParsingEventHandler(this.dataGridView1_CellParsing);
            this.dataGridView1.RowValidating += new System.Windows.Forms.DataGridViewCellCancelEventHandler(this.dataGridView1_RowValidating);
            // 
            // nameDataGridViewTextBoxColumn
            // 
            this.nameDataGridViewTextBoxColumn.DataPropertyName = "Name";
            this.nameDataGridViewTextBoxColumn.HeaderText = "Name";
            this.nameDataGridViewTextBoxColumn.Name = "nameDataGridViewTextBoxColumn";
            // 
            // Column1
            // 
            this.Column1.HeaderText = "Data (unbound)";
            this.Column1.Name = "Column1";
            // 
            // domainModelBindingSource
            // 
            this.domainModelBindingSource.DataSource = typeof(DomainModel);
            // 
            // button1
            // 
            this.button1.Location = new System.Drawing.Point(12, 12);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(168, 23);
            this.button1.TabIndex = 1;
            this.button1.Text = "Update Data Source";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(461, 214);
            this.Controls.Add(this.button1);
            this.Controls.Add(this.dataGridView1);
            this.Name = "Form1";
            this.Text = "Form1";
            ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.domainModelBindingSource)).EndInit();
            this.ResumeLayout(false);
    }

    #endregion

    private System.Windows.Forms.DataGridView dataGridView1;
    private System.Windows.Forms.BindingSource domainModelBindingSource;
    private System.Windows.Forms.Button button1;
    private System.Windows.Forms.DataGridViewTextBoxColumn nameDataGridViewTextBoxColumn;
    private System.Windows.Forms.DataGridViewTextBoxColumn Column1;

    #endregion
}

internal sealed class DomainModel
{
    public DomainModel() { }

    public DomainModel(string name)
    {
        this.Name = name;
    }

    public string Name { get; set; }
}

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

Solution

  • I had mentioned that I already "fixed the problem" which wasn't completely accurate. I fixed it in the test application, effectively validating the solution I was planning on using, but hadn't actually implemented it. Once I tried to I realized why I did things the way I did originally: The data that is used in the unbound column is a read-only collection of objects and in order to set them I need to call a separate method on the object. Best practices and all...

    Well, read only properties of course can't be edited in the DGV so I was dead in the water. This made me go BACK to my original design and while researching I stumbled on this column/blog by Brian Noyes.

    He mentions the event RowsAdded and that's all I needed! Here is the solution for my problem:

    private void dataGridView1_RowsAdded(object sender, 
        DataGridViewRowsAddedEventArgs e)
    {
        if (dataGridView1.Rows[e.RowIndex].IsNewRow)
        {
            return;
        }
    
        dataGridView1[1, e.RowIndex].Value = 
            (dataGridView1.Rows[e.RowIndex].DataBoundItem as DomainModel).Name;
    }
    

    I like this solution because it's placing the data on the Cell the same way it would if it were bound, it then hands off formatting responsibility to CellFormatting and I can do my work. Likewise I can parse the data and construct the required objects to pass to my bound objects "SetListData" method.

    All good!