0

I have a DataGrid with a List<Foo> as the datasource. I want the datagrid to use my custom control as the editor for the Foo.Value property. I believe is the purpose of the UIHintAttribute, but it has no effect. I know I can explicitly make columns, and assign ValueColumn.ColumnEdit = new FooValueEditor();, but I'm trying to side step what would be a lot of UI code and depend on the grid inferring columns from public properties.

    class Foo
    {
        public string Name { get; set; }

        [UIHint("FooValueEditor", "WinForms")]
        public int Value { get; set; }
    }

    public class FooValueEditor : System.Windows.Forms.TextBox
    {
        public FooValueEditor() : base()
        {
            ...
        }
    }

I've tried providing the full namespace to my custom editor. I find many examples of the attribute used in Asp.NET, but the attribute constructor takes a presentationLayer parameter which:

Can be set to "HTML", "Silverlight", "WPF", or "WinForms".

I hope it supports WinForms. Am I doing something wrong? Is this not possible?

Edit:

Regarding comment "What is a DataGrid here". I am using a set of third party controls which I incorrectly assumed inherited from DataGrid. It supports the validation attributes (This is their documentation for Aps.Net, but it seems at least partly currect for WinForms also) in the same namespace so I was hopeful UIHint might be supported. Looks like I should open a ticket directly with the third party provider, but in the mean time, the answer below will help me implement this myself if I choose to.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
blackboxlogic
  • 557
  • 6
  • 13
  • What is a *DataGrid* here? A DataGridView? You can build a custom DataGridViewColumn that makes use of a Custom / User Control for data entry /editing: [How to: Host Controls in Windows Forms DataGridView Cells](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-host-controls-in-windows-forms-datagridview-cells) -- Tag your question to specify the correct Platform and objects you're working with. – Jimi Mar 09 '21 at 22:50
  • You need to write code to make the framework use the `UIHint` attribute. To get some idea, take a look at this post: [DataAnnotations attributes for DataGridView in Windows Forms](https://stackoverflow.com/a/59885956/3110834). For custom editors (other than the built-in column types) you need to created your custom datagridview column type, like the example in the docs shared by Jimi. – Reza Aghaei Mar 10 '21 at 09:53
  • Well, the answer that I shared is just valid for Windows Forms DataGrdiView, neither for a 3rd party control, nor for a WPF or WinForms DataGrid. For the 3rd party control, you most likely need to refer to their documentation. It's also a good idea that you edit the question add add the correct control in the tags. – Reza Aghaei Mar 10 '21 at 15:59
  • Also the link to devexprss grid is pointing to a ASP.NET WebForm control. Quite confusing question. – Reza Aghaei Mar 10 '21 at 16:04
  • I've edited further in response to your feedback. – blackboxlogic Mar 10 '21 at 17:18
  • For reference, my post on the third party [support forum](https://supportcenter.devexpress.com/ticket/details/t980771) – blackboxlogic Mar 10 '21 at 17:20

1 Answers1

0

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

Here in this post I'm going to extend the answer that I've shared in my other post about DataAnnotations attributes for DataGridView in Windows Forms so you can have the following features:

  • Visibility of the column: controlled by [Browsable] attribute. You can also rely on AutoGenerateField property of the [Display] attribute.
  • Header text of the column: controlled by Name of the [Display] attribute.
  • Order of the column: controlled by Order of the [Display] attribute.
  • Tooltip of the column: controlled by [DisplayFormat] 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:

enter image description here

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.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398