Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Event circularity

I find myself quite often in the following situation:

I have a user control which is bound to some data. Whenever the control is updated, the underlying data is updated. Whenever the underlying data is updated, the control is updated. So it's quite easy to get stuck in a never ending loop of updates (control updates data, data updates control, control updates data, etc.).

Usually I get around this by having a bool (e.g. updatedByUser) so I know whether a control has been updated programmatically or by the user, then I can decide whether or not to fire off the event to update the underlying data. This doesn't seem very neat.

Are there some best practices for dealing with such scenarios?

EDIT: I've added the following code example, but I think I have answered my own question...?

public partial class View : UserControl
{
    private Model model = new Model();

    public View()
    {
        InitializeComponent();
    }

    public event EventHandler<Model> DataUpdated;

    public Model Model
    {
        get
        {
            return model;
        }
        set
        {
            if (value != null)
            {
                model = value;
                UpdateTextBoxes();
            }
        }
    }

    private void UpdateTextBoxes()
    {
        if (InvokeRequired)
        {
            Invoke(new Action(() => UpdateTextBoxes()));
        }
        else
        {
            textBox1.Text = model.Text1;
            textBox2.Text = model.Text2;
        }
    }

    private void textBox1_TextChanged(object sender, EventArgs e)
    {
        model.Text1 = ((TextBox)sender).Text;
        OnModelUpdated();
    }

    private void textBox2_TextChanged(object sender, EventArgs e)
    {
        model.Text2 = ((TextBox)sender).Text;
        OnModelUpdated();
    }

    private void OnModelUpdated()
    {
        DataUpdated?.Invoke(this, model);
    }
}

public class Model
{
    public string Text1 { get; set; }
    public string Text2 { get; set; }
}

public class Presenter
{
    private Model model;
    private View view;

    public Presenter(Model model, View view)
    {
        this.model = model;
        this.view = view;

        view.DataUpdated += View_DataUpdated;
    }

    public Model Model
    {
        get
        {
            return model;
        }
        set
        {
            model = value;
            view.Model = model;
        }
    }

    private void View_DataUpdated(object sender, Model e)
    {
        //This is fine.
        model = e;

        //This causes the circular dependency.
        Model = e;
    }
}
like image 238
digital_fate Avatar asked Jul 25 '16 10:07

digital_fate


3 Answers

One option would be to stop the update in case the data didn't change since the last time. For example if the data were in form of a class, you could check if the data is the same instance as the last time the event was triggered and if that is the case, stop the propagation.

This is what many MVVM frameworks do to prevent raising PropertyChanged event in case the property didn't actually change:

private string _someProperty = "";
public string SomeProperty
{
    get
    {
        return _someProperty;
    }
    set
    {
        if ( _someProperty != value )
        {
           _someProperty = value;
           RaisePropertyChanged();
        }
    }
}

You can implement this concept similarly for Windows Forms.

like image 66
Martin Zikmund Avatar answered Oct 05 '22 05:10

Martin Zikmund


What you're looking for is called Data Binding. It allows you to connect two or more properties, so that when one property changes others will be updated auto-magically.

In WinForms it's a little bit ugly, but works like a charm in cases such as yours. First you need a class which represents your data and implements INotifyPropertyChanged to notify the controls when data changes.

public class ViewModel : INotifyPropertyChanged
{

    private string _textFieldValue;
    public string TextFieldValue {
        get
        {
            return _textFieldValue;
        }

        set
        {
            _textFieldValue = value;
            NotifyChanged();
        }
    }

    public void NotifyChanged()
    {
        if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(null));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Than in your Form/Control you bind the value of ViewModel.TextFieldValue to textBox.Text. This means whenever value of TextFieldValue changes the Text property will be updated and whenever Text property changes TextFieldValue will be updated. In other words the values of those two properties will be the same. That solves the circular loops issue you're encountering.

public partial class Form1 : Form
{
    public ViewModel ViewModel = new ViewModel();

    public Form1()
    {
        InitializeComponent();
        // Connect:  textBox1.Text <-> viewModel.TextFieldValue
        textBox1.DataBindings.Add("Text", ViewModel , "TextFieldValue");
    }
}

If you need to modify the values from outside of the Form/Control, simply set values of the ViewModel

form.ViewModel.TextFieldValue = "new value";

The control will be updated automatically.

like image 30
Mariusz Jamro Avatar answered Oct 05 '22 05:10

Mariusz Jamro


You should look into MVP - it is the preferred design pattern for Winforms UI. http://www.codeproject.com/Articles/14660/WinForms-Model-View-Presenter

using that design pattern gives you a more readable code in addition to allowing you to avoid circular events.

in order to actually avoid circular events, your view should only export a property which once it is set it would make sure the txtChanged_Event would not be called.

something like this:

public string UserName
    {
        get
        {
            return txtUserName.Text;
        }
        set
        {
            txtUserName.TextChanged -= txtUserName_TextChanged;
            txtUserName.Text = value;
            txtUserName.TextChanged += txtUserName_TextChanged;
        }
    }

or you can use a MZetko's answer with a private property

like image 20
gilmishal Avatar answered Oct 05 '22 05:10

gilmishal