如何使用WinForms数据绑定正确触发UserControl中值的更改?

4
我在我的WinForms应用程序中创建了两个UserControl。其中一个包含一个TextBox(现在我们称其为TextEntryControl),另一个应该使用我输入到此TextBox中的值来执行内部操作(启用按钮并在单击该按钮时使用该值) - 现在我们称其为TextUsingControl。
然而,我无法通过DataBinding实现这一点。
首先(天真的)尝试:
我向TextEntryControl添加了一个string属性,如下所示:
public string MyStringProperty { get; set; }

然后我使用UI设计工具将TextBoxText属性绑定到这个MyStringProperty上,生成了一个textEntryControlBindingSource。我在构造函数中添加了InitializeComponents()后面:

textEntryControlBindingSource.Add(this);

我向TextUsingControl添加了相同的属性,在我使用这两个控件的外部UI中,我将TextUsingControl的字符串属性绑定到TextEntryControl的一个属性上,并适当地更新绑定源:

textEntryControlBindingSource.Add(textEntryControl1);

我在TabControl的不同选项卡上使用了不同的控件,但是这种机制只有第一次输入文本后切换到其他控件时才有效。

下一个尝试

我创建了一个简单的字符串包装类:

public sealed class StringWrapper {

    public string Content { get; set; }
}

在我的文本输入控件中,我将文本框绑定到这个字符串包装器,并更改属性,使其如下所示:
public string MyStringProperty {
    get {
        return _stringWrapper.Content;
    }
    set {
        _stringWrapper.Content = value;
    }
}

我在外部控件中使用了类似的方法来处理TabControl - 使用StringWrapper将两个用户控件的MyStringProperty绑定在一起。
结果:相同。但这是合理的,因为委托给包装器的外部属性不会得到通知。
第三次尝试
这个有点奏效,但我认为这是一个丑陋的解决方法。
我完全放弃了MyStringProperty,并通过一个属性传递包装器对象本身,再次将其传递给绑定源。
public StringWrapper MyStringWrapper {
    get {
        return stringWrapperBindingSource.Cast<StringWrapper>().FirstOrDefault();
    }
    set {
        stringWrapperBindingSource.Clear();
        if(value != null) stringWrapperBindingSource.Add(value);
    }
}

现在我只创建了一个单独的StringWrapper对象,并在InitializeComponent()之后将其设置为两个用户控件。 INotifyPropertyChanged 作为跟进:我也尝试了INotifyPropertyChanged,如MSDN上所述。但这也没有帮助。 我想要实现的目标 我希望两个用户控件都具有MyStringProperty,当我输入TextEntryControl中的文本框时,该属性应更新并正确通知附加到它的任何绑定源。当TextUsingControl的属性更改时,应更新它本身。
后半部分很容易,我只需在属性的set部分添加适当的逻辑即可,但我在第一部分遇到了麻烦。
我习惯于Eclipse的JFace数据绑定,在那里可以使用PropertyChangeSupportPropertyChangeListener实现此功能-在那里,我只需将适当的事件触发代码添加到setter中,并在设置数据绑定时使用BeanProperties.value()
1个回答

2

这是一个适当的属性实现和适当的数据绑定的组合。

(1) 属性实现:

属性不需要很复杂,它可以像你的 naïve 方法一样简单,但是必要的部分是它应该提供属性更改通知。您可以使用通用的INotifyPropertyChanged机制或Windows Forms特定的PropertyNameChanged命名事件模式。在两种情况下,您都不能使用C# 自动属性功能,而必须手动实现(使用显式支持字段)。以下是一个示例实现:

string myStringProperty;
public string MyStringProperty
{
    get { return myStringProperty; }
    set
    {
        if (myStringProperty == value) return;
        myStringProperty = value;
        var handler = MyStringPropertyChanged;
        if (handler != null) handler(this, EventArgs.Empty);
    }
}

public event EventHandler MyStringPropertyChanged;

(2) 数据绑定:

将单个控件属性绑定到单个对象属性称为简单数据绑定,可以通过Control.DataBindings实现。您可以查看ControlBindingsCollection.Add方法 / Binding构造函数重载Binding类的属性/方法/事件以获取更多信息。

Binding所做的基本上是创建源对象属性和目标对象属性之间的链接(单向或双向)。请注意“属性”这个词 - 这正是开箱即用支持的内容。但是,通过以下简单的帮助程序(在我的回答中提供,如何在TextChange时从false设置TextBox.Enabled为true?),您可以轻松创建单向表达式绑定:

public static void Bind(this Control target, string targetProperty, object source, string sourceProperty, Func<object, object> expression)
{
    var binding = new Binding(targetProperty, source, sourceProperty, true, DataSourceUpdateMode.Never);
    binding.Format += (sender, e) => e.Value = expression(e.Value);
    target.DataBindings.Add(binding);
}

这是一个完整的演示示例:
using System;
using System.Windows.Forms;

namespace Samples
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            var form = new Form();
            var splitView = new SplitContainer { Dock = DockStyle.Fill, Parent = form };
            var textEntry = new TextEntryControl { Dock = DockStyle.Fill, Parent = splitView.Panel1 };
            var textConsumer = new TextConsumingControl { Dock = DockStyle.Fill, Parent = splitView.Panel2 };
            textConsumer.DataBindings.Add("MyStringProperty", textEntry, "MyStringProperty", true, DataSourceUpdateMode.Never);
            Application.Run(form);
        }
    }

    class TextEntryControl : UserControl
    {
        TextBox textBox;
        public TextEntryControl()
        {
            textBox = new TextBox { Parent = this, Left = 16, Top = 16 };
            textBox.DataBindings.Add("Text", this, "MyStringProperty", true, DataSourceUpdateMode.OnPropertyChanged);
        }

        string myStringProperty;
        public string MyStringProperty
        {
            get { return myStringProperty; }
            set
            {
                if (myStringProperty == value) return;
                myStringProperty = value;
                var handler = MyStringPropertyChanged;
                if (handler != null) handler(this, EventArgs.Empty);
            }
        }

        public event EventHandler MyStringPropertyChanged;
    }

    class TextConsumingControl : UserControl
    {
        Button button;
        public TextConsumingControl()
        {
            button = new Button { Parent = this, Left = 16, Top = 16, Text = "Click Me" };
            button.Bind("Enabled", this, "MyStringProperty", value => !string.IsNullOrEmpty(value as string));
        }

        string myStringProperty;
        public string MyStringProperty
        {
            get { return myStringProperty; }
            set
            {
                if (myStringProperty == value) return;
                myStringProperty = value;
                var handler = MyStringPropertyChanged;
                if (handler != null) handler(this, EventArgs.Empty);
            }
        }

        public event EventHandler MyStringPropertyChanged;
    }

    public static class BindingUtils
    {
        public static void Bind(this Control target, string targetProperty, object source, string sourceProperty, Func<object, object> expression)
        {
            var binding = new Binding(targetProperty, source, sourceProperty, true, DataSourceUpdateMode.Never);
            binding.Format += (sender, e) => e.Value = expression(e.Value);
            target.DataBindings.Add(binding);
        }
    }
}

正如您所看到的,通过这一行代码,可以建立textEntrytextConsumer之间的请求(单向)绑定:

textConsumer.DataBindings.Add("MyStringProperty", textEntry, "MyStringProperty", true, DataSourceUpdateMode.Never);

在演示中,你可以看到另一个有趣的点是,如果你愿意,你实际上也可以数据绑定内部控件属性。整个内部文本框和属性之间的同步是通过这一行代码实现的:

textBox.DataBindings.Add("Text", this, "MyStringProperty", true, DataSourceUpdateMode.OnPropertyChanged);

并且启用TextConsumingControl内部按钮:

button.Bind("Enabled", this, "MyStringProperty", value => !string.IsNullOrEmpty(value as string));

当然,最后两件事情是可选的,你可以在属性设置器中完成,但知道这种选择存在还是很酷的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接