Introduction
Oftentimes, user interface code gets cluttered by event handlers that manipulate state, e.g. when button A is clicked, disable button B, etc.
Data binding is a great way to simplify user interface logic. This article assumes a familiarity with .NET data binding with WinForms. (If you are unfamiliar with this concept, information is readily available on CodeProject or through MSDN -- it is an elegant technique, one that I use for many purposes).
Background
Anyone familiar with WinForms development has undoubtedly seen code to manage Enabled
state of various UI elements.
The traditional way consists of careful event handlers and property initialization.
public Form1()
{
InitializeComponent();
button1.Enabled = true;
button2.Enabled = false;
panel1.Enabled = false;
}
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
button2.Enabled = true;
panel1.Enabled = true;
}
private void button2_Click(object sender, EventArgs e)
{
button1.Enabled = true;
button2.Enabled = false;
panel1.Enabled = false;
}
This can be simplified with a binding, e.g.
public Form1()
{
InitializeComponent();
button1.Enabled = true;
button2.Enabled = false;
panel1.DataBindings.Add(new Binding("Enabled", button2, "Enabled"));
}
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
button2.Enabled = true;
}
private void button2_Click(object sender, EventArgs e)
{
button1.Enabled = true;
button2.Enabled = false;
}
Unfortunately, since button 1 and 2 are inversely related, i.e. when button 1 is enabled, button 2 is disabled, we can't add a traditional binding. This happens more often than you may consider, e.g. connect and disconnect buttons behave exactly this way. The InvertedBinding
construct defined in this article provides a seamless solution to this problem.
Using the Code
The attached project is a VS 2008 solution targeting .NET 2.0 Framework, but the relevant code is included here. Simply snag the InvertedBinding
class and you're good to go.
Usage Example
public Form1()
{
InitializeComponent();
button2.DataBindings.Add(InvertedBinding.Create(button1, "Enabled"));
panel1.DataBindings.Add(new Binding("Enabled", button2, "Enabled"));
}
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
}
private void button2_Click(object sender, EventArgs e)
{
button1.Enabled = true;
}
Now, the entire UI enabled state is driven off of button1.Enabled
. There is also no need to set initial values (in the Designer or in the code).
Implementation
using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows.Forms;
namespace InverseBinding
{
public class InvertedBinding : INotifyPropertyChanged, IDisposable
{
#region Private Fields
private readonly object _dataSource;
private readonly EventInfo _changedEvent;
private readonly MethodInfo _getAccessor;
private readonly MethodInfo _setAccessor;
#endregion
#region Constructors
protected InvertedBinding
(object dataSource, string dataSourceBoundPropertyName)
{
if ((dataSource == null) || (dataSourceBoundPropertyName == null))
{
throw new ArgumentNullException();
}
_dataSource = dataSource;
EventInfo propertyChangedEvent =
_dataSource.GetType().GetEvent(string.Format("{0}Changed",
dataSourceBoundPropertyName));
if ((propertyChangedEvent != null) && (typeof(EventHandler).
IsAssignableFrom(propertyChangedEvent.EventHandlerType)))
{
_changedEvent = propertyChangedEvent;
_changedEvent.AddEventHandler(_dataSource,
new EventHandler(OnDataSourcePropertyChanged));
}
PropertyInfo dataSourceBoundProperty =
_dataSource.GetType().GetProperty(dataSourceBoundPropertyName);
if (dataSourceBoundProperty == null)
{
throw new MissingMemberException(string.Format(
"Could not find property '{0}.{1}'",
_dataSource.GetType().FullName,
dataSourceBoundPropertyName));
}
_getAccessor = dataSourceBoundProperty.GetGetMethod();
if (_getAccessor == null)
{
throw new MissingMethodException(string.Format(
"No get accessor for '{0}'", dataSourceBoundProperty.Name));
}
if (!typeof(bool).IsAssignableFrom(_getAccessor.ReturnType))
{
throw new ArgumentException(
string.Format(
"Class only works on boolean properties,
'{0}' is not of type bool",
dataSourceBoundProperty.Name));
}
_setAccessor = dataSourceBoundProperty.GetSetMethod();
}
#endregion
public static Binding Create(object dataSource, string propertyName)
{
return new Binding(propertyName,
new InvertedBinding(dataSource, propertyName), "InvertedProperty");
}
public bool InvertedProperty
{
get
{
return !GetDataBoundValue();
}
set
{
if (_setAccessor == null)
{
return;
}
bool curVal = InvertedProperty;
if (curVal == value)
{
_setAccessor.Invoke(_dataSource, new object[] { !value });
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs("InvertedProperty"));
}
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region IDisposable Members
public void Dispose()
{
if (_changedEvent != null)
{
_changedEvent.RemoveEventHandler
(_dataSource, new EventHandler(OnDataSourcePropertyChanged));
}
}
#endregion
private void OnDataSourcePropertyChanged(object sender, EventArgs e)
{
InvertedProperty = !GetDataBoundValue();
}
private bool GetDataBoundValue()
{
return (bool) _getAccessor.Invoke(_dataSource, null);
}
}
}
Points of Interest
Implementing INotifyPropertyChanged
is important in order for the framework (e.g. Button
or Panel
) to update their state when the bound property changes.
The code shows many examples of reflection, including dynamic event discovery and registration. This sort of thing does not excite me anymore since it is second nature at this point, but a beginner may find it interesting. :)
It is possible to adapt this code to target .NET 3.0 which would allow for getting rid of magic strings using lambda.
History
- Initial revision
- Minor updates (tailored exception messages, added XML documentation)