Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent infinite recursion when using events to bind UI elements to fields?

Tags:

c#

winforms

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

like image 959
Billy ONeal Avatar asked Apr 22 '10 15:04

Billy ONeal


2 Answers

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);
        }
    }
}
like image 148
ParmesanCodice Avatar answered Oct 12 '22 23:10

ParmesanCodice


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;
like image 41
Roman Kupin Avatar answered Oct 13 '22 00:10

Roman Kupin