This project is read-only.

Easier way to update GUI properties

Apr 1, 2011 at 9:25 PM
Edited Apr 5, 2011 at 4:50 PM

I have to update certain GUI elements manually in my application based on Dependents/Independents, and doing so has proven somewhat tedius. Looking at the existing UpdateControls for WinForms, a pattern seems to emerge... code that looks roughly like this:

	Dependent _dep1;
	Dependent _dep2;
	Dependent _dep3;

	public void ControlConstructor()
	{
		_dep1 = new Dependent(Update1);
		_dep2 = new Dependent(Update2);
		_dep3 = new Dependent(Update3);
	}
	public void Dispose()
	{
		_dep1.Dispose();
		_dep2.Dispose();
		_dep3.Dispose();
	}
	void Application_Idle(object sender, EventArgs e)
	{
		_dep1.OnGet();
		_dep2.OnGet();
		_dep3.OnGet();
	}
	protected override void OnHandleCreated(EventArgs e)
	{
		// Register idle-time updates.
		Application.Idle += new EventHandler(Application_Idle);
		base.OnHandleCreated(e);
	}
	protected override void OnHandleDestroyed(EventArgs e)
	{
		// Unregister idle-time updates.
		Application.Idle -= new EventHandler(Application_Idle);
		base.OnHandleDestroyed(e);
	}

For some reason UpdateListBox disposes some Dependents in OnHandleDestroyed even though they were not created in OnHandleCreated... but ignoring that, this is the pattern. For my own code I do something simpler, setting up the Application.Idle handler in my constructor. Still, all this work is tedius. So I came up with a "GuiUpdateHelper" that simplifies the code to:

	GuiUpdateHelper _updater;

	public void ControlConstructor()
	{
		_updater = new GuiUpdateHelper(false, Update1, Update2, Update3);
	}
	public void Dispose()
	{
		_updater.Dispose();
	}
	protected override void OnHandleCreated(EventArgs e)
	{
		_updater.StartOnCurrentThread();
		base.OnHandleCreated(e);
	}
	protected override void OnHandleDestroyed(EventArgs e)
	{
		_updater.Stop();
		base.OnHandleDestroyed(e);
	}

Or better yet:

	GuiUpdateHelper _updater;

	public void ControlConstructor()
	{
		_updater = new GuiUpdateHelper(true, Update1, Update2, Update3);
	}
	public void Dispose()
	{
		_updater.Dispose();
	}

This is a lot easier than managing the Application.Idle event and the Dependents separately. Here's the code:

/// <summary>
/// Helps implement WinForms Update Controls by automatically updating a list
/// of dependents (which usually correspond to actual properties of a
/// control) when an Application_Idle event arrives.
/// </summary>
/// <remarks>
/// Remember to dispose this object when the control (or other
/// object) that uses it is disposed.
/// </remarks>
public class GuiUpdateHelper : IDisposable
{
	/// <summary>Initializes GuiUpdateHelper.</summary>
	/// <param name="startNow">Whether to call StartOnCurrentThread now.</param>
	/// <param name="updaters">A list of methods that perform updates.</param>
	/// <remarks>
	/// If startNow is false, a Control should override OnHandleCreated() to 
	/// call StartOnCurrentThread and override OnHandleDestroyed() to call
	/// Stop. Here are possible reasons for doing so: 
	/// (1) the constructor could theoretically be called on a different
	///     thread than the control is created on.
	/// (2) it is unnecessary to keep a control's properties up-to-date before
	///     the control is actually created.
	/// (3) the control might not allow the properties to be changed before
	///     the control is actually created.
	/// </remarks>
	public GuiUpdateHelper(bool startNow, params Action[] updaters)
	{
		_dependents = new Dependent[updaters.Length];
		for (int i = 0; i < updaters.Length; i++)
			_dependents[i] = new Dependent(updaters[i]);
		if (startNow)
			StartOnCurrentThread();
	}

	Dependent[] _dependents;
	bool _started;

	/// <summary>This method is called after updating all dependents,
	/// regardless of whether anything changed.</summary>
	/// <remarks>The boolean is true if any of the dependents were
	/// out-of-date.</remarks>
	public event Action<bool> OnUpdate;

	/// <summary>Forces any out-of-date dependents to update, and 
	/// fires the OnUpdate event.</summary>
	virtual public void UpdateNow()
	{
		bool updated = false;
		for (int i = 0; i < _dependents.Length; i++)
		{
			if (!_dependents[i].IsUpToDate)
			{
				updated = true;
				_dependents[i].OnGet();
			}
		}
		if (OnUpdate != null)
			OnUpdate(updated);
	}

	/// <summary>Returns true if the Application.Idle event is being handled.</summary>
	public bool IsStarted
	{
		get { return _started; }
	}
	/// <summary>Subscribes to Application.Idle if a subscription wasn't done already.</summary>
	public bool StartOnCurrentThread()
	{
		if (!_started)
		{
			_started = true;
			Application.Idle += new EventHandler(Application_Idle);
			return true;
		}
		return false;
	}
	/// <summary>Removes the subscription to Application.Idle.</summary>
	public void Stop()
	{
		_started = false;
		Application.Idle -= new EventHandler(Application_Idle);
	}
	void Application_Idle(object sender, EventArgs e)
	{
		UpdateNow();
	}
	public virtual void Dispose()
	{
		Stop();
		for (int i = 0; i < _dependents.Length; i++)
			_dependents[i].Dispose();
	}
}
Apr 1, 2011 at 9:34 PM
Edited Apr 1, 2011 at 10:06 PM

Update: Oh crap, I just realized this won't work in general, because the property getters need to call OnGet() on the Dependent.... quick fix below.

A bug in UpdateControls.Forms just occurred to me. What if a control is created and THEN one of the Get events is set? e.g. suppose GetEnabled is null, then I create the control, then I set GetEnabled to a proper handler. Since GetEnabled has no known dependencies, it will never be called. Am I right?

Apr 1, 2011 at 10:13 PM
The following change allows OnGet() to be called on the required dependent:
	public GuiUpdateHelper(bool startNow, params Action[] updaters)
	{
		_updaters = updaters;
		_dependents = new Dependent[updaters.Length];
		for (int i = 0; i < updaters.Length; i++)
			_dependents[i] = new Dependent(updaters[i]);
		if (startNow)
			StartOnCurrentThread();
	}
	
	Action[] _updaters;
	Dependent[] _dependents;
	bool _started;

	public void OnGet(Action updater)
	{
		for (int i = 0; i < _dependents.Length; i++)
			if (_updaters[i] == updater)
				_dependents[i].OnGet();
	}

Example usage in a Control that uses GuiUpdateHelper:

	public new bool Enabled
	{
		get {
			_updater.OnGet(UpdateEnabled);
			return base.Enabled;
		}
	}

It works but it's less than ideal: The loop wastes time, and the array of updaters can't be garbage collected (we must keep it around because Dependent's _update method is not exposed publicly).

 

Apr 2, 2011 at 4:53 PM

You are correct about that bug in UpdateControls.Forms. If the event is not set before the handle is created, then it will never be called. The way to fix that is to make the event itself independent. If you call OnGet before firing the event, and OnSet in add/remove, then it will fix that. However, since it is typical to wire up events in the designer rather than code, I wonder if this is worth fixing.

I think the helper could be simpler if the control was still responsible for creating the Dependents. The helper could take care of OnIdle, OnHandleCreated, and OnHandleDestroyed, but the control could still call OnGet directly.

In addition, I would drop the startNow parameter. Always assume that you want to start tracking right now. You list some valid reasons why you might not want it to start right now, but I think those would be edge cases. If you run into those edge cases, then you could just not use the helper.

Having said all that, I wonder how common it is for someone to wire up their own control. Are we the only two who have done so? If it's a common thing to do, then this helper belongs in the Forms dll.

Apr 4, 2011 at 5:42 PM

I think most "production" WinForms apps will need something that isn't in the standard toolbox. In my case, I made an UpdateTrackBar, and I need to keep graphical objects up-to-date on a map control written in C++.

You're right about the helper if you want it limited to WinForms controls: one could give the helper a list of dependents to update, and a pointer to this, and its job is to add/remove an Application.Idle handler in the HandleCreated/HandleDestroyed events, and to dispose the Dependents when it is disposed. However, this assumes the GuiUpdateHelper will always be managing a WinForms control. My map control is, as it happens, wrapped into a WinForms control, but it's conceivable that some users may need to manage non-WinForms controls. But it doesn't matter to me; you can add the helper to the UC library or not, as-is or with changes.

Regarding the bug I was talking about, I thought that if Dependent.OnGet() was called before any dependencies were established (e.g. because the control's GetXxx event is not wired up yet), then the Dependent would discover no dependencies and therefore never call _update again. Based on this reasoning, I thought, "if I look at the control's properties in the debugger before the events are wired up, the control will not function, because those properties call OnGet, and discover no dependencies". However, this experiment did not cause my test control to fail. I wonder why.

Apr 4, 2011 at 7:58 PM

I think it might be working because the control's properties are defined by the base class. They have backing storage there, and observing them will not cause a call to Dependent.OnGet(). The call to Dependent.OnGet() is only in the Application_Idle, which in this case is called only after the GetXXX events are wired up.

I'm still thinking about how best to include this helper. It doesn't need to cover every scenario, it just needs to make the common scenario as easy as possible. If you run into a different scenario, you can always do things without the helper. That's why I'd like to drop the startNow flag, and it's also why I would not be adverse to tying it to a Control.

In fact, it might be best to make this a generic base class. UpdateControl<T> : T where T : Control. This would allow you to inject it into the class hierarchy without giving up your base class.

Apr 4, 2011 at 11:22 PM

OnGet is also called by the "new" properties in the derived class (new bool Enabled, etc). I wondered if maybe you had special logic for Dependents with no dependencies. And now I wonder if the .NET framework has any way to detect that code is running inside a debugger watch (a watch window or watch tooltip could conceivably screw up dependency detection.)

Your idea is unsupported. A .NET Generic class cannot be derived from one of its type parameters. But as a separate object, it can subscribe to the Control's events.

Hey, I blogged about UpdateControls: http://loyc-etc.blogspot.com/2011/04/update-controls.html

Apr 5, 2011 at 4:11 PM

Ah, yes. I forgot about those properties. I'm traveling right now, so I didn't look at the source before posting.

Also, too bad about the generic inheritance restriction. I used to use that technique all the time in C++, but I guess it won't work in C#.

Thanks for the writeup. Good post.