[WinForn] BindingSource 使用 ErrorProvider 驗證欄位

畫面上有 BindingNavigator、DataGridView,它們的資料都來自 BindingSource,我希望透過上方的編輯區塊進行編輯、驗證的互動,不是 GridView,畫面設計如下圖:

我想要做的功能很簡:單當移動"列"時,驗證當下的所有欄位,驗證失敗不准離開

開發環境

  • WIndows 10 Enterprise
  • VS 2017 Enterprise

事前準備

ErrorProvider

首先要來做驗證,這裡我採用的是 ErrorProvider,它的用法很簡單

設定錯誤:errorProvider1.SetError(textBox1, "Not an integer value.");
清除錯誤:errorProvider1.Clear(); 詳請請參考文件

https://docs.microsoft.com/zh-tw/dotnet/framework/winforms/controls/display-error-icons-for-form-validation-with-wf-errorprovider
https://docs.microsoft.com/zh-tw/dotnet/framework/winforms/controls/view-errors-within-a-dataset-with-wf-errorprovider-component

 

為了快速設定控制項的 Error,我先用 Validator.TryValidateObject 找出驗證失敗的欄位,再用 ErrorProvider1.SetError() 將問題的欄位呈現出來

public static bool ValidateControl<T>(this Control                      container,
                                      T                                 instance,
                                      ErrorProvider                     errorProvider,
                                      out ICollection<ValidationResult> validationResults)
    where T : class, new()
{
    var innerControls = new Dictionary<string, Control>();
    container.GetAllInnerControls<T>(ref innerControls);
 
    errorProvider.Clear();
    var validationContext = new ValidationContext(instance, null, null);
    validationResults = new List<ValidationResult>();
    var isValid = Validator.TryValidateObject(instance, validationContext, validationResults, true);
    if (isValid)
    {
        return isValid;
    }
 
    foreach (var validationResult in validationResults)
    {
        foreach (var member in validationResult.MemberNames)
        {
            if (!innerControls.ContainsKey(member))
            {
                continue;
            }
 
            var control = innerControls[member];
            errorProvider.SetError(control, validationResult.ErrorMessage);
        }
    }
 
    return isValid;
}

 

ValidateControl 擴充方法擴充了 Control 型別,跑遞迴搜尋所有的控制項,設計畫面時可以用一個容器控制項管理其他的控制項,就像下圖這樣

控制項的命名要用 "欄位_" 開頭,比如 Age_TextBox,我會用物件的屬性去找符合的控制項

代碼如下

var isValid = group1.ValidateControl(insertRequest, errorProvider, out var errorValidationResults);
if (!isValid)
{
	return;
}

完整代碼如下

https://github.com/yaochangyu/sample.dotblog/blob/master/ModelValidation/WinForm/Lab.BindingSourceValid/WindowsFormsApp1/ValidatorExtension.cs

https://github.com/yaochangyu/sample.dotblog/blob/master/ModelValidation/WinForm/Lab.BindingSourceValid/WindowsFormsApp1/Form1.cs
 

畫面設計與資料細節

接著要設計 ViewModel

public class InsertRequest : INotifyPropertyChanged
{
    private Guid? _id;
    private string _name;
    private DateTime? _birthday;
    private int? _age;
 
    [Required]
    public Guid? Id
    {
        get => this._id;
        set
        {
            if (value.Equals(this._id)) return;
 
            this._id = value;
            this.OnPropertyChanged();
        }
    }
 
    [StringLength(50)]
    [Required]
    public string Name
    {
        get => this._name;
        set
        {
            if (value == this._name) return;
 
            this._name = value;
            this.OnPropertyChanged();
        }
    }
 
    [Required]
    public DateTime? Birthday
    {
        get => this._birthday;
        set
        {
            if (value.Equals(this._birthday)) return;
 
            this._birthday = value;
            this.OnPropertyChanged();
        }
    }
 
    [Range(1, 150)]
    [Required]
    public int? Age
    {
        get => this._age;
        set
        {
            if (value == this._age) return;
 
            this._age = value;
            this.OnPropertyChanged();
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

 

接著拉出控制項,然後修正控制項的命名規則

 

最主要的關鍵是利用 BindingSource.PositionChanged、DataGridView.SelectionChanged 事件,記住當下的 BindingSource.Position,當移動"列"時,驗證當下的所有欄位,驗證失敗不准離開

private void InsertRequest_BindingSource_PositionChanged(object sender, EventArgs e)
{
    var insertRequestBinding = (BindingSource) sender;
    var insertRequest        = this._previousInsertRequest;
    var errorProvider        = this.ErrorProvider;
 
    if (this._isDelete)
    {
        this._isDelete = false;
        this.SetPreviousRow();
        this.StayCurrentRow();
        errorProvider.Clear();
        return;
    }
 
    insertRequestBinding.EndEdit();
    this.RegisterChangeRowEvent(false);
    var isValid = this.ValidateControl(insertRequest, errorProvider, out var errorValidationResults);
    if (!isValid)
    {
        this.StayCurrentRow();
        this.RegisterChangeRowEvent(true);
        return;
    }
 
    this.RegisterChangeRowEvent(true);
    this.SetPreviousRow();
}
 
private void InsertRequest_DataGridView_SelectionChanged(object sender, EventArgs e)
{
    if (this._isDelete)
    {
        this._isDelete = false;
        return;
    }
 
    this.RegisterChangeRowEvent(false);
    this.StayCurrentRow();
    this.RegisterChangeRowEvent(true);
}

 

演示畫面

 

在實作的過程中碰到比較特別的點

DataGridView 指定目前所在資料行,要使用 CurrentCell 屬性

insertRequestGrid.CurrentCell = previousCell;

 

就是下圖紅框的箭頭

DataGridView 反白特定的列,要用 Row.Selected屬性

insertRequestGrid
                .Rows[this._previousBindingPosition]
                .Selected = true;

 

DateTimePicker 要呈現空白,要用 CustomFormat、Format 兩個屬性

this.Birthday_DateTimePicker.CustomFormat = " ";
this.Birthday_DateTimePicker.Format       =  DateTimePickerFormat.Custom;

 

然後在特定的事件設定 CustomFormat 為 "yyyy/MM/dd hh:mm:ss"

private static readonly string DateTimeFormat = "yyyy/MM/dd hh:mm:ss";
public Form1()
{
    ....
    this.Birthday_DateTimePicker.Format       =  DateTimePickerFormat.Custom;
    this.Birthday_DateTimePicker.CustomFormat =  DateTimeFormat;
    this.Birthday_DateTimePicker.ValueChanged += this.Birthday_DateTimePicker_ValueChanged;
}
private void Birthday_DateTimePicker_ValueChanged(object sender, EventArgs e)
{
    this.Birthday_DateTimePicker.CustomFormat = DateTimeFormat;
}
 
private void Add_Button_Click(object sender, EventArgs e)
{
    this.InsertRequest_BindingSource.AddNew();
    this.Birthday_DateTimePicker.CustomFormat = " ";
}

 

專案位置
https://github.com/yaochangyu/sample.dotblog/tree/master/ModelValidation/WinForm/Lab.BindingSourceValid
 

 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo