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 onAutoGenerateField
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:
Here are building blocks of the example:
- There is a
UIHintMappings
class which is responsible to map the UI hints to differentDataGridViewColumn
types. Each UI Hint will be mapped to aFunc
(a factory method) which creates an instance of the desiredDataGridViewColumn
. For exampleText
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
- Create a Windows Forms Application.
- Drop an instance of
DataGridView
on Form1 (and set it to dock in parent container) -
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; } }
- 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); } }
- 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()); } }
- 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; } }
- Double click on the
Form1
in design mode and handleLoad
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
-
You can add support for data annotations validations as well. To do so you can implement
IDataErrorInfo
interface usingValidator
class, the same way that I’ve done it in DataAnnotations Validation attributes for Windows Forms. -
You can add support for Enum columns easily using
DataGridViewComboBoxColumn
like this post. -
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
forbool
properties.
hello dear
I am a beginner in Asp MVC , if you have resources and examples Email for me . tnx
omid
Nice tutorial, thanks. however, All apps i develop from ground up with localization in mind. How can it be integrated in this since i donnot use resx localization, intead i load from text file