DataAnnotations Attributes in Windows Forms

There is no built-in support for Data Annotation attributes in Windows Forms, but considering how those attributes work, you can extend the frameworks and contrls to use those attributes in Windows Forms.

In a previous post, I showed how you can extend Windows Forms to use DataAnnotation Validation Attributes; here in this post I am going to show how you can control DataGridView column name, column order, visibility, tooltip, control type, and format using DataAnnotation Attributes:

  • Visibility of the column: controlled by [Browsable] attribute. You can also rely on AutoGenerateField property of the [Display] attribuyte.
  • Header text of the column: controlled by Name of the [Display] attribute.
  • Order of the column: controlled by Order of the [Display] attribute.
  • Format of the column: controlled by [DisplayFormat] attribute.
  • Tooltip of the column: controlled by Description of the [Display] attribute.
  • Type of the column: Controlled by [UIHint] attribute.

So after setting up data annotations attribute on the model, if you setup datagridveiw like this this.dataGridView1.Bind(list, true); you will see:

DataAnnotations Validation Attributes in Windows Forms

Here are building blocks of the example:

  • There is a UIHintMappings class which is responsible to map the UI hints to different DataGridViewColumn types. Each UI Hint will be mapped to a Func (a factory method) which creates an instance of the desired DataGridViewColumn. For example Text will be mapped to ()=>new DataGridViewTextBoxColumn(). You can add or remove mappings based on your requirements.

  • There is a Bind<T> extension method which is responsible to generate columns of the DataGridview using a list, by applying the data annotations attributes. You can change the logic of column creation here; for example adding support for a new attribute.

  • For each non-standard column types, you need to create your own column type by following the instruction/example here: How to: Host Controls in Windows Forms DataGridView Cells and then add the mapping.

Example

  1. Create a Windows Forms Application.
  2. Drop an instance of DataGridView on Form1 (and set it to dock in parent container)
  3. Add a Person.cs file to the project and paste the following code into the file:

    using System;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    
    public class Person
    {
        [Display(Name = "Id")]
        [Browsable(false)]
        public int? Id { get; set; }
    
        [Display(Name = "First Name", Description = "First name.", Order = 1)]
        [UIHint("TextBox")]
        public string FirstName { get; set; }
    
        [Display(Name = "Last Name", Description = "Last name", Order = 2)]
        [UIHint("TextBox")]
        public string LastName { get; set; }
    
        [Display(Name = "Birth Date", Description = "Date of birth.", Order = 4)]
        [DisplayFormat(DataFormatString = "yyyy-MM-dd")]
        [UIHint("Calendar")]
        public DateTime BirthDate { get; set; }
    
        [Display(Name = "Homepage", Description = "Url of homepage.", Order = 5)]
        [UIHint("Link")]
        public string Url { get; set; }
    
        [Display(Name = "Member", Description = "Is member?", Order = 3)]
        [UIHint("CheckBox")]
        public bool IsMember { get; set; }
    }
    
  4. Create a code file named DataGridViewCalendarColumn.cs and paste the following code into the file (this is based on MS Docs example, just changed a little to respect change the format):
    using System;
    using System.Windows.Forms;
    
    public class DataGridViewCalendarColumn : DataGridViewColumn
    {
        public DataGridViewCalendarColumn() : base(new DataGridViewCalendarCell())
        {
        }
        public override DataGridViewCell CellTemplate
        {
            get
            {
                return base.CellTemplate;
            }
            set
            {
                // Ensure that the cell used for the template is a CalendarCell.
                if (value != null && 
                !value.GetType().IsAssignableFrom(typeof(DataGridViewCalendarCell)))
                {
                    throw new InvalidCastException("Must be a CalendarCell");
                }
                base.CellTemplate = value;
            }
        }
    }
    
    public class DataGridViewCalendarCell : DataGridViewTextBoxCell
    {
        public DataGridViewCalendarCell()
            : base()
        {
            // Use the short date format.
            // this.Style.Format = "d";
        }
        public override void InitializeEditingControl(int rowIndex, object
            initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
        {
            // Set the value of the editing control to the current cell value.
            base.InitializeEditingControl(rowIndex, initialFormattedValue,
                dataGridViewCellStyle);
            DataGridViewCalendarEditingControl ctl =
                DataGridView.EditingControl as DataGridViewCalendarEditingControl;
            // Use the default row value when Value property is null.
            if (this.Value == null)
            {
                ctl.Value = (DateTime)this.DefaultNewRowValue;
            }
            else
            {
                ctl.Value = (DateTime)this.Value;
            }
        }
    
        public override Type EditType
        {
            get
            {
                // Return the type of the editing control that CalendarCell uses.
                return typeof(DataGridViewCalendarEditingControl);
            }
        }
    
        public override Type ValueType
        {
            get
            {
                // Return the type of the value that CalendarCell contains.
    
                return typeof(DateTime);
            }
        }
    
        public override object DefaultNewRowValue
        {
            get
            {
                // Use the current date and time as the default value.
                return DateTime.Now;
            }
        }
    }
    
    class DataGridViewCalendarEditingControl : DateTimePicker, 
        IDataGridViewEditingControl
    {
        DataGridView dataGridView;
        private bool valueChanged = false;
        int rowIndex;
    
        public DataGridViewCalendarEditingControl()
        {
            //this.Format = DateTimePickerFormat.Short;
        }
    
        // Implements the IDataGridViewEditingControl.EditingControlFormattedValue
        // property.
        public object EditingControlFormattedValue
        {
            get
            {
                return this.Value.ToShortDateString();
            }
            set
            {
                if (value is String)
                {
                    try
                    {
                        // This will throw an exception of the string is
                        // null, empty, or not in the format of a date.
                        this.Value = DateTime.Parse((String)value);
                    }
                    catch
                    {
                        // In the case of an exception, just use the
                        // default value so we're not left with a null
                        // value.
                        this.Value = DateTime.Now;
                    }
                }
            }
        }
    
        // Implements the
        // IDataGridViewEditingControl.GetEditingControlFormattedValue method.
        public object GetEditingControlFormattedValue(
            DataGridViewDataErrorContexts context)
        {
            return EditingControlFormattedValue;
        }
    
        // Implements the
        // IDataGridViewEditingControl.ApplyCellStyleToEditingControl method.
        public void ApplyCellStyleToEditingControl(
            DataGridViewCellStyle dataGridViewCellStyle)
        {
            this.Font = dataGridViewCellStyle.Font;
            this.CalendarForeColor = dataGridViewCellStyle.ForeColor;
            this.CalendarMonthBackground = dataGridViewCellStyle.BackColor;
            if (!string.IsNullOrEmpty(dataGridViewCellStyle.Format))
            {
                this.Format = DateTimePickerFormat.Custom;
                this.CustomFormat = dataGridViewCellStyle.Format;
            }
            else
            {
                this.Format = DateTimePickerFormat.Short;
            }
        }
    
        // Implements the IDataGridViewEditingControl.EditingControlRowIndex
        // property.
        public int EditingControlRowIndex
        {
            get
            {
                return rowIndex;
            }
            set
            {
                rowIndex = value;
            }
        }
    
        // Implements the IDataGridViewEditingControl.EditingControlWantsInputKey
        // method.
        public bool EditingControlWantsInputKey(
            Keys key, bool dataGridViewWantsInputKey)
        {
            // Let the DateTimePicker handle the keys listed.
            switch (key & Keys.KeyCode)
            {
                case Keys.Left:
                case Keys.Up:
                case Keys.Down:
                case Keys.Right:
                case Keys.Home:
                case Keys.End:
                case Keys.PageDown:
                case Keys.PageUp:
                    return true;
                default:
                    return !dataGridViewWantsInputKey;
            }
        }
    
        // Implements the IDataGridViewEditingControl.PrepareEditingControlForEdit
        // method.
        public void PrepareEditingControlForEdit(bool selectAll)
        {
            // No preparation needs to be done.
        }
    
        // Implements the IDataGridViewEditingControl
        // .RepositionEditingControlOnValueChange property.
        public bool RepositionEditingControlOnValueChange
        {
            get
            {
                return false;
            }
        }
    
        // Implements the IDataGridViewEditingControl
        // .EditingControlDataGridView property.
        public DataGridView EditingControlDataGridView
        {
            get
            {
                return dataGridView;
            }
            set
            {
                dataGridView = value;
            }
        }
    
        // Implements the IDataGridViewEditingControl
        // .EditingControlValueChanged property.
        public bool EditingControlValueChanged
        {
            get
            {
                return valueChanged;
            }
            set
            {
                valueChanged = value;
            }
        }
    
        // Implements the IDataGridViewEditingControl
        // .EditingPanelCursor property.
        public Cursor EditingPanelCursor
        {
            get
            {
                return base.Cursor;
            }
        }
    
        protected override void OnValueChanged(EventArgs eventargs)
        {
            // Notify the DataGridView that the contents of the cell
            // have changed.
            valueChanged = true;
            this.EditingControlDataGridView.NotifyCurrentCellDirty(true);
            base.OnValueChanged(eventargs);
        }
    }
    
  5. Add a UIHintMappings.cs file to the project and paste the following code into the file:
    using System;
    using System.Collections.Generic;
    using System.Windows.Forms;
    
    public class UIHintMappings
    {
        public static Dictionary<string, Func<DataGridViewColumn>> DataGridViewColumns
        {
            get;
        }
        static UIHintMappings()
        {
            DataGridViewColumns = new Dictionary<string, Func<DataGridViewColumn>>();
            DataGridViewColumns.Add("TextBox", 
                () => new DataGridViewTextBoxColumn());
            DataGridViewColumns.Add("CheckBox", 
                () => new DataGridViewCheckBoxColumn(false));
            DataGridViewColumns.Add("TreeStateCheckBox", 
                () => new DataGridViewCheckBoxColumn(true));
            DataGridViewColumns.Add("Link", 
                () => new DataGridViewLinkColumn());
            DataGridViewColumns.Add("Calendar", 
                () => new DataGridViewCalendarColumn());
        }
    }
    
  6. Add DataGridViewExtensions.cs file to the project and paste the following code into the file:
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Windows.Forms;
    
    public static class DataGridViewExtensions
    {
        public static void Bind<T>(this DataGridView grid, IList<T> data,
            bool autoGenerateColumns = true)
        {
            if (autoGenerateColumns)
            {
                var properties = TypeDescriptor.GetProperties(typeof(T));
                var metedata = properties.Cast<PropertyDescriptor>().Select(p => new
                {
                    Name = p.Name,
                    HeaderText = p.Attributes.OfType<DisplayAttribute>()
                        .FirstOrDefault()?.Name ?? p.DisplayName,
                    ToolTipText = p.Attributes.OfType<DisplayAttribute>()
                        .FirstOrDefault()?.GetDescription() ?? p.Description,
                    Order = p.Attributes.OfType<DisplayAttribute>()
                        .FirstOrDefault()?.GetOrder() ?? int.MaxValue,
                    Visible = p.IsBrowsable,
                    ReadOnly = p.IsReadOnly,
                    Format = p.Attributes.OfType<DisplayFormatAttribute>()
                        .FirstOrDefault()?.DataFormatString,
                    Type = p.PropertyType,
                    UIHint = p.Attributes.OfType<UIHintAttribute>()
                        .FirstOrDefault()?.UIHint
                });
                var columns = metedata.OrderBy(m => m.Order).Select(m =>
                {
                    DataGridViewColumn c;
                    if(!string.IsNullOrEmpty( m.UIHint) && 
                    UIHintMappings.DataGridViewColumns.ContainsKey(m.UIHint))
                    {
                        c = UIHintMappings.DataGridViewColumns[m.UIHint].Invoke();
                    }
                    else
                    {
                        c = new DataGridViewTextBoxColumn();
                    }
                    c.DataPropertyName = m.Name;
                    c.Name = m.Name;
                    c.HeaderText = m.HeaderText;
                    c.ToolTipText = m.ToolTipText;
                    c.DefaultCellStyle.Format = m.Format;
                    c.ReadOnly = m.ReadOnly;
                    c.Visible = m.Visible;
                    return c;
                });
                grid.Columns.Clear();
                grid.Columns.AddRange(columns.ToArray());
            }
            grid.DataSource = data;
        }
    }
    
  7. Double click on the Form1 in design mode and handle Load event using the following code:
    private void Form1_Load(object sender, EventArgs e)
    {
        var list = new List<Person>()
        {
            new Person()
            {
                Id= 1, FirstName= "Mario", LastName= "Speedwagon",
                BirthDate = DateTime.Now.AddYears(-30).AddMonths(2).AddDays(5),
                IsMember = true, Url ="https://Mario.example.com"
            },
            new Person()
            {
                Id= 1, FirstName= "Petey", LastName= "Cruiser",
                BirthDate = DateTime.Now.AddYears(-20).AddMonths(5).AddDays(1),
                IsMember = false, Url ="https://Petey.example.com"
            },
            new Person()
            {
                Id= 1, FirstName= "Anna", LastName= "Sthesia",
                BirthDate = DateTime.Now.AddYears(-40).AddMonths(3).AddDays(8),
                IsMember = true, Url ="https://Anna.example.com"
            },
        };
    
        this.dataGridView1.Bind(list, true);
    }
    

Run the project and, there you go! You can see how attributes helped to generate columns.

Points of improvement for future readers

  1. You can add support for data annotations validations as well. To do so you can implement IDataErrorInfo interface using Validator class, the same way that I’ve done it in DataAnnotations Validation attributes for Windows Forms.

  2. You can add support for Enum columns easily using DataGridViewComboBoxColumn like this post.

  3. You can improve the mapping in a way that, if there isn’t a mapping defined for a UIHint, then as a fallbackm look into the type of the column, for example use DataGridViewCheckBoxColumn for bool properties.

You May Also Like

About the Author: Reza Aghaei

I’ve been a .NET developer since 2004. During these years, as a developer, technical lead and architect, I’ve helped organizations and development teams in design and development of different kind of applications including LOB applications, Web and Windows application frameworks and RAD tools. As a teacher and mentor, I’ve trained tens of developers in C#, ASP.NET MVC and Windows Forms. As an interviewer I’ve helped organizations to assess and hire tens of qualified developers. I really enjoy learning new things, problem solving, knowledge sharing and helping other developers. I'm usually active in .NET related tags in stackoverflow to answer community questions. I also share technical blog posts in my blog as well as sharing sample codes in GitHub.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *