tags:

views:

101

answers:

6

The following seems to be a relatively common pattern (to me, not to the community at large) to bind a string variable to the contents of a TextBox.

class MyBackEndClass
{
    public event EventHandler DataChanged;
    string _Data;
    public string Data
    {
        get { return _Data; }
        set
        {
            _Data = value;
            //Fire the DataChanged event
        }
    }
}

class SomeForm : // Form stuff
{
    MyBackEndClass mbe;
    TextBox someTextBox;
    SomeForm() 
    {
        someTextBox.TextChanged += HandleTextBox();
        mbe.DataChanged += HandleData();
    }

    void HandleTextBox(Object sender, EventArgs e)
    {
        mbe.Data = ((TextBox)sender).Text;
    }

    void HandleData(Object sender, EventArgs e)
    {
        someTextBox.Text = ((MyBackEndClass) sender).Data;
    }
}

The problem is that changing the TextBox fires the changes the data value in the backend, which causes the textbox to change, etc. That runs forever.

Is there a better design pattern (other than resorting to a nasty boolean flag) that handles this case correctly?

EDIT: To be clear, in the real design the backend class is used to synchronize changes between multiple forms. Therefore I can't just use the SomeTextBox.Text property directly.

Billy3

A: 

You could check sender inside set accessor of MyBackEndClass's class Data property If its SomeForm - just dont raise event.

Roman Kupin
But I need the event to fire. The event needs to fire to update other forms in the application that use data from SomeForm.
Billy ONeal
+1  A: 

Use bindings.

someTestBox.BindingContext.Add(new Binding("Text", mbe, "Data"));

Edit: My appologies, it's BindingContext and it should work if you bind all your forms to the back-end object, when one updates the BEO, it'll update all forms attached to it (and it wont explode recursively.)

Aren
Err.. I'm a bit confused. System.Forms.TextBox does not expose a "Bindings" property.
Billy ONeal
OK -- I'll look into this.
Billy ONeal
A: 

I think you're going to be stuck with a Boolean flag, or better yet some sort of enum value, that you pass into your HandleTextBox method.

You could compare old and new string values to see if the value has actually changed and warrants changing the value displayed in the text box.

AaronS
+2  A: 

Ok I've wrote some code, but you might not like it :)

public class DataChangedEventArgs : EventArgs
{
    public string Data { get; set; }

    public DataChangedEventArgs(string data)
    {
        Data = data;
    }
}
public delegate void DataChangedEventHander(DataChangedEventArgs e);
public class BackEnd
{
    public event DataChangedEventHander OnDataChanged;
    private string _data;
    public string Data
    {
        get { return _data; }
        set
        {
            _data = value;
            RaiseOnDataChanged();
        }
    }

    private static readonly object _sync = new object();
    private static BackEnd _instance;
    public static BackEnd Current
    {
        get
        {
            lock (_sync)
            {
                if (_instance == null)
                    _instance = new BackEnd();
                return _instance;
            }
        }
    }
    private void RaiseOnDataChanged()
    {
        if(OnDataChanged != null)
            OnDataChanged(new DataChangedEventArgs(Data));
    }
}
public class ConsumerControl
{
    public event EventHandler OnTextChanged;
    private string _text;
    public string Text
    {
        get
        {
            return _text;
        }
        set
        {
            _text = value;
            if (OnTextChanged != null)
                OnTextChanged(this, EventArgs.Empty);
        }
    }
}
public class Consumer
{
    public ConsumerControl Control { get; set; }

    public Consumer()
    {
        Control = new ConsumerControl();
        BackEnd.Current.OnDataChanged += NotifyConsumer;
        Control.OnTextChanged += OnTextBoxDataChanged;
    }

    private void OnTextBoxDataChanged(object sender, EventArgs e)
    {
        NotifyBackEnd();
    }

    private void NotifyConsumer(DataChangedEventArgs e)
    {
        Control.Text = e.Data;
    }
    private void NotifyBackEnd()
    {
        // unsubscribe
        BackEnd.Current.OnDataChanged -= NotifyConsumer;
        BackEnd.Current.Data = Control.Text;
        // subscribe again
        BackEnd.Current.OnDataChanged += NotifyConsumer;
    }
}
public class BackEndTest
{
    public void Run()
    {
        var c1 = new Consumer();
        var c2 = new Consumer();
        c1.Control.Text = "1";
        BackEnd.Current.Data = "2";
    }
}

The main idia is here:

// unsubscribe
BackEnd.Current.OnDataChanged -= NotifyConsumer;
BackEnd.Current.Data = Control.Text;
// subscribe again
BackEnd.Current.OnDataChanged += NotifyConsumer;
Roman Kupin
You're right, I don't like it, but it's better than the boolean flag idea. +1.
Billy ONeal
+3  A: 

Even though I can't replicate this problem, I have an idea on how to fix it.

Currently you actually have a DataSetEvent and not a DataChangedEvent.

class MyBackEndClass
{
    public event EventHandler DataChanged;

    private string data = string.Empty;

    public string Data
    {
        get { return this.data; }
        set
        {   
            // Check if data has actually changed         
            if (this.data != value)
            {
                this.data = value;
                //Fire the DataChanged event
            }
        }
    }
}

This should stop the recursion, because now you get TextBoxTextChanged->DataChanged->TextBoxChanged ->Data hasn't changed events stop here.

EDIT: Maybe move this code into the TextBox to remove the flicker:
Replace your System.Windows.Forms.TextBox's with this:

class CleverTextBox : TextBox
{
    private string previousText = string.Empty;

    public CleverTextBox() : base()
    {
        // Maybe set the value here, not sure if this is necessary..?
        this.previousText = base.Text;
    }

    public override OnTextChanged(EventArgs e)
    {
        // Only raise the event if the text actually changed
        if (this.previousText != base.Text)
        {                
            this.previousText = this.Text;
            base.OnTextChanged(e);
        }
    }
}
ParmesanCodice
Ah yes, this prevents the recursion, but it still causes the textbox to flicker each time the user enters a character as the control is redrawn. Not ideal but it works. +1
Billy ONeal
@Billy ONeal what if you move the code into a custom TextBox as I've edited my answer to indicate? As I can't replicate your problem I cannot test this, sorry.
ParmesanCodice
Instead of subclassing TextBox, or modifying the MyBackEndClass, you could move the != test to HandleTextBox() and HandleData()
ajs410
@ajs410 Good point. I'm just wondering if the OP doesn't have a lot of these controls. It might be easier for him to update them in one place. +1 Nice comment.
ParmesanCodice
A: 

It's a little dirty...but you could try parsing the Environment.StackTrace property for a previous call to TextChanged. I think it's a bit less dirty than the boolean though, which to me cries out for thread safety.

ajs410
Windows forms cannot be multithreaded, which means there's no problem there.
Billy ONeal
Uh...do you mean that GUI controls can only be changed on the thread that created the handle? The backend class is not a control, so there's no reason you can't change the mbe.Data property from a BackgroundWorker thread.
ajs410