简体   繁体   中英

How to correctly trigger the change of a value in a UserControl using WinForms Data Binding?

I created two UserControl s in my WinForms application. One contains a TextBox (let's call this TextEntryControl for now) and the other one should use the value I entered into this TextBox to do internal stuff (enable a button and use the value when this button is clicked) - let's call this TextUsingControl .

However, I don't get this right via DataBinding.

First (naive) approach

I added a string property to TextEntryControl like this:

public string MyStringProperty { get; set; }

Then I used the UI designer to bind the TextBox 's Text property to this MyStringProperty , resulting in a textEntryControlBindingSource . I added to the constructor right behind the InitializeComponents() :

textEntryControlBindingSource.Add(this);

I added the same property to the TextUsingControl , and in my outer UI where I use the two controls, I bound the TextUsingControl 's string property to the one of the TextEntryControl , and updated the binding source appropriately:

textEntryControlBindingSource.Add(textEntryControl1);

I use both controls on different tabs of a TabControl , and the mechanism only works once, when I first enter text into the text box and then switch to the other control.

Next try

I created a simple wrapper class for strings:

public sealed class StringWrapper {

    public string Content { get; set; }
}

In my text entry control, I bound the text box to this string wrapper, and changed the property to look like this:

public string MyStringProperty {
    get {
        return _stringWrapper.Content;
    }
    set {
        _stringWrapper.Content = value;
    }
}

I did a similar thing in the outer control with the TabControl - use a StringWrapper to bind both MyStringProperty of both user controls to.

Result: Same. But this is logical since the outer property that delegates to the wrapper doesn't get notified.

Third try

This one works, sort of, but I think this is an ugly workaround.

I ditched the MyStringProperty altogether and pass in the wrapper object itself via a property that, again, passes it down to the binding source:

public StringWrapper MyStringWrapper {
    get {
        return stringWrapperBindingSource.Cast<StringWrapper>().FirstOrDefault();
    }
    set {
        stringWrapperBindingSource.Clear();
        if(value != null) stringWrapperBindingSource.Add(value);
    }
}

Now I only create one single StringWrapper object and set it to both user controls right after InitializeComponent() .

INotifyPropertyChanged

As a follow up: I tried INotifyPropertyChanged as well as described on MSDN. This did not help either.

What I want to achieve

I want both user controls to have a MyStringProperty , and when the text I enter into TextEntryControl 's text box changes, the property should update and correctly notify any binding sources it is attached to. The TextUsingControl should update itself when its property changes.

The second half is easy, I just add the appropriate logic into the set part of the property, but I am having trouble with the first one.

I am used to Eclipse's JFace Data Binding, where this functionality can be achieved with PropertyChangeSupport and PropertyChangeListener - here, I just add the appropriate event firing code to the setter and I can use BeanProperties.value() when setting up the data binding.

It's a combination of a proper property implementation and proper data binding.

(1) Property implementation:

The property does not need to be complex. It could be simple type as in your naïve approach, but the essential part is that it should provide property change notification . You can use the general INotifyPropertyChanged mechanism or Windows Forms specific PropertyNameChanged named event pattern. In both cases, you cannot use C# auto property feature and have to implement it manually (with an explicit backing field). Here is a sample implementation:

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) Data binding:

Binding single control property to a single object property is called simple data binding and is achieved via Control.DataBindings . You can take a look at ControlBindingsCollection.Add method / Binding Constructor overloads and Binding class properties/methods/events for more info.

What the Binding does is basically creating a link (one or two way) between source object property and target object property. Note the word property - that's exactly what is supported out of the box. But with the following simple helper method (presented in my answer to How to set Textbox.Enabled from false to true on TextChange? ) you can easily create one way expression like binding:

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

Here is a full working demo:

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

As you can see, the requested (one way) binding between textEntry and textConsumer is established with this single line:

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

Another interesting point you can see in the demo is that if you wish, you can actually data bind internal control properties too. The whole synchronization between the TextEntryControl internal text box and the property is achieved with this single line:

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

and the TextConsumingControl internal button enabling:

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

Of course the last two things are optional, you can do that in the property setter, but it's kind of cool to know that such option exists.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM