Windows 表单中的 DataAnnotation 属性 - 如何在 WinForms 中使用 UIHint 属性

DataAnnotation Attribute in Windows Forms - How to use UIHint Attribute with WinForms

我有一个 DataGrid 和一个 List<Foo> 作为数据源。我希望数据网格使用我的自定义控件作为 Foo.Value 属性 的编辑器。我相信是UIHintAttribute的目的,但是没有效果。我知道我可以明确地制作列,并分配 ValueColumn.ColumnEdit = new FooValueEditor();,但我试图避开 很多 的 UI 代码并依赖于网格从 public 属性推断列。

    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()
        {
            ...
        }
    }

我试过为我的自定义编辑器提供完整的命名空间。我发现许多 Asp.NET 中使用的属性示例,但属性构造函数采用 presentationLayer 参数,其中:

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

希望它支持WinForms。难道我做错了什么?这不可能吗?

编辑:

关于评论“这里的 DataGrid 是什么”。我在同一个命名空间中使用了一组 third party controls which I incorrectly assumed inherited from DataGrid. It supports the validation attributes(这是他们针对 Aps.Net 的文档,但它似乎至少部分适用于 WinForms)所以我希望 UIHint 可能得到支持.看起来我应该直接与第三方提供商开票,但与此同时,如果我选择的话,下面的答案将帮助我自己实施。

built-in 不支持 Windows 表单中的数据注释属性(包括 UI提示),但考虑到这些属性的工作方式,您可以扩展框架和控件以在 Windows 表单中使用这些属性。

在此 post 我将扩展我在其他 post 中分享的关于 DataAnnotations attributes for DataGridView in Windows Forms 的答案,以便您可以拥有以下功能:

  • 列的可见性:由 [Browsable] attribute. You can also rely on AutoGenerateField property of the [Display] 属性控制。
  • Header 列的文本:由 [Display] 属性的 Name 控制。
  • 列的顺序:由 [Display] 属性的 Order 控制。
  • 列的工具提示:由[DisplayFormat]属性控制。
  • 列的类型:由 [UIHint] 属性控制。

所以在模型上设置数据注释属性后,如果你像这样设置 datagridveiw this.dataGridView1.Bind(list, true); 你会看到:

以下是示例的构建块:

  • 有一个 UIHintMappings class 负责将 UI 提示映射到不同的 DataGridViewColumn 类型。每个 UI 提示都将映射到一个 Func(工厂方法),它创建所需 DataGridViewColumn 的一个实例。例如 Text 将映射到 ()=>new DataGridViewTextBoxColumn()。您可以根据需要添加或删除映射。

  • 有一个 Bind<T> 扩展方法负责通过应用数据注释属性使用列表生成 DataGridview 的列。您可以在此处更改列创建的逻辑;例如添加对新属性的支持。

  • 对于每个 non-standard 列类型,您需要按照此处的 instruction/example 创建自己的列类型:How to: Host Controls in Windows Forms DataGridView Cells,然后添加映射。

例子

  1. 创建 Windows 表单申请。

  2. 在 Form1 上放置一个 DataGridView 的实例(并将其设置为停靠在 parent 容器中)

  3. 在项目中添加一个Person.cs文件,将以下代码粘贴到文件中:

    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. 创建一个名为 DataGridViewCalendarColumn.cs 的代码文件,并将以下代码粘贴到文件中(这是基于 MS Docs example,只是为了尊重格式做了一点改动):

    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. 在项目中添加一个UIHintMappings.cs文件,将以下代码粘贴到文件中:

    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. DataGridViewExtensions.cs文件添加到项目中并将以下代码粘贴到文件中:

    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. 在设计模式中双击 Form1 并使用以下代码处理 Load 事件:

    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);
    }
    

运行 项目,好了!您可以看到属性如何帮助生成列。

未来读者的改进点

  1. 您还可以添加对数据注释验证的支持。为此,您可以使用 Validator class 实现 IDataErrorInfo 接口,就像我在 DataAnnotations Validation attributes for Windows Forms.

    中所做的一样
  2. 您可以使用 DataGridViewComboBoxColumn 轻松添加对枚举列的支持,例如 this post

  3. 您可以通过以下方式改进映射,如果没有为 UIHint 定义映射,那么作为后备方法查看列的类型,对于例如使用 DataGridViewCheckBoxColumn 作为 bool 属性。