首页 > 解决方案 > WinForms BindingComplete 由另一个属性触发

问题描述

我有一个 TextBox,其 Text 属性绑定到名为 Name 的 ViewModel 属性,还有一个 Button,其 Enable 属性绑定到名为 IsBusy 的 ViewModel 属性。

并且 TexBox Binding 实现了 BindingComplete 如果属性引发异常,它会更改 TextBox 的背景颜色。

问题是当 IsBusy 属性发生更改且 IsBusy 属性绑定未订阅时也会引发 BindingComplete,并且 BindingComplete 事件参数具有“名称”的 BindingField 和 BindingMember,并且此绑定的关联控件是 TextBox。

为什么 IsBusy 属性会引发绑定到 Name 属性的 BindingComplete?

因此,在我强制 TextBox 绑定调用 WriteValue() 以验证出现错误时按预期设置背景颜色的属性,并为 IsBusy 属性分配一个真值以指示它正忙后,它会设置背景颜色回到 White,因为 IsBusy 无一例外地引发了 TextBox 的 BindingComplete。

仅供参考,我故意引发 PropertyChanged 事件并引发异常,因为我希望输入不正确的值以供用户查看。

这只是我遇到的问题的一个演示。发生错误时,我有更复杂的业务需求。因此,如果您能解释为什么会发生这种情况以及如何防止或修复它,我们将不胜感激。

在此处输入图像描述

public partial class BindingTestForm : Form
{
    private TestViewModel viewModel = new TestViewModel();
    public BindingTestForm()
    {
        InitializeComponent();
        
        var textBoxTextBinding = new Binding(nameof(TextBox.Text), viewModel, nameof(TestViewModel.Name), true, DataSourceUpdateMode.OnPropertyChanged);
        textBoxTextBinding.BindingComplete += TextBoxTextBinding_BindingComplete;
        NameTextBox.DataBindings.Add(textBoxTextBinding);

        var enableBinding = new Binding(nameof(Control.Enabled), viewModel, nameof(TestViewModel.IsBusy));
        enableBinding.Format += EnableBinding_Format;
        SaveButton.DataBindings.Add(enableBinding);

        Load += BindingTestForm_Load;
    }

    private void EnableBinding_Format(object sender, ConvertEventArgs e)
    {
        e.Value = !(bool)e.Value;
    }

    private void TextBoxTextBinding_BindingComplete(object sender, BindingCompleteEventArgs e)
    {
        if (e.Exception != null)
            NameTextBox.BackColor = Color.Red;
        else
            NameTextBox.BackColor = Color.White;
    }

    private async void BindingTestForm_Load(object sender, EventArgs e)
    {
        viewModel.IsBusy = true;
        await Task.Run(async () =>
        {
            await Task.Delay(2000);
        });
        viewModel.IsBusy = false;
    }

    public class TestViewModel: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TestViewModel.Name)));
                if (string.IsNullOrWhiteSpace(_name))
                    throw new Exception("Please enter package name.");
                if (_name.Length > 5)
                    throw new Exception("Max length 5.");
            }
        }
        public bool _isBusy = false;
        public bool IsBusy 
        { 
            get => _isBusy; 
            set 
            { 
                _isBusy = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TestViewModel.IsBusy)));
            } 
        } 
    }

    private async void SaveButton_Click(object sender, EventArgs e)
    {
        NameTextBox.DataBindings[nameof(TextBox.Text)].WriteValue();

        viewModel.IsBusy = true;
        await Task.Run(async () =>
        {
            await Task.Delay(2000);
        });
        viewModel.IsBusy = false;
    }
}


partial class BindingTestForm
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.NameTextBox = new System.Windows.Forms.TextBox();
        this.SaveButton = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // NameTextBox
        // 
        this.NameTextBox.Location = new System.Drawing.Point(13, 13);
        this.NameTextBox.Name = "NameTextBox";
        this.NameTextBox.Size = new System.Drawing.Size(100, 20);
        this.NameTextBox.TabIndex = 0;
        // 
        // SaveButton
        // 
        this.SaveButton.Location = new System.Drawing.Point(13, 75);
        this.SaveButton.Name = "SaveButton";
        this.SaveButton.Size = new System.Drawing.Size(75, 23);
        this.SaveButton.TabIndex = 1;
        this.SaveButton.Text = "Save";
        this.SaveButton.UseVisualStyleBackColor = true;
        this.SaveButton.Click += new System.EventHandler(this.SaveButton_Click);
        // 
        // BindingTestForm
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(800, 450);
        this.Controls.Add(this.SaveButton);
        this.Controls.Add(this.NameTextBox);
        this.Name = "BindingTestForm";
        this.Text = "FlowLayoutTestForm";
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.TextBox NameTextBox;
    private System.Windows.Forms.Button SaveButton;
}

标签: c#winformsbinding

解决方案


我必须承认我不知道为什么会这样。

作为替代解决方案,我建议您创建一个能够显示红色边框和工具提示的文本框控件。

public class TextBoxEx : TextBox
{
    [DllImport("user32")]
    private static extern IntPtr GetWindowDC(IntPtr hwnd);
    private const int WM_NCPAINT = 0x85;

    private readonly ToolTip _toolTip = new ToolTip();
    private bool _hasError;

    private string _toolTipText;
    public string ToolTipText
    {
        get {
            return _toolTipText;
        }
        set {
            if (value != _toolTipText) {
                _toolTipText = value;
                if (String.IsNullOrEmpty(_toolTipText)) {
                    _toolTip.Hide(this);
                    _hasError = false;
                } else {
                    _toolTip.Show(_toolTipText, this, 3000);
                    _hasError = true;
                }

                // This is required to update the border color immediately.
                var m = Message.Create(Handle, WM_NCPAINT, IntPtr.Zero, IntPtr.Zero);
                WndProc(ref m); 
            }
        }
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if (_hasError && m.Msg == WM_NCPAINT) {
            var dc = GetWindowDC(Handle);
            using (Graphics g = Graphics.FromHdc(dc)) {
                g.DrawRectangle(Pens.Red, 0, 0, Width - 1, Height - 1);
            }
        }
    }
}

将工具提示文本设置为不同于 null 或空的内容会显示工具提示并显示红色边框。

然后数据模型(MVVM 上下文中的视图模型)必须提供错误消息并实现INotifyPropertyChanged. 例子:

public class Model : INotifyPropertyChanged
{
    private string _text;
    public string Text
    {
        get { return _text; }
        set {
            if (value != _text) {
                _text = value;
                OnPropertyChanged(nameof(Text));
                OnPropertyChanged(nameof(TextErrorMessage));
            }
        }
    }


    public string TextErrorMessage
    {
        get {
            return _text?.Length == 4
            ? null
            : "Please enter a text of length 4";
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

现在将 的Text属性绑定TextBoxEx到模型的属性,并将Text的属性绑定到模型的属性。设置两个绑定并将模型对象分配给表单上组件的属性。ToolTipTextTextBoxExTextErrorMessageOnPropertyChangedDataSourceBindingSource

现在您有了一个功能齐全的 MVVM 模式,除了将模型分配给绑定源之外,不需要表单中的任何代码。


推荐阅读