Note this answer uses WinForm and was written before C# had 'NameOf()'
Thanks to Oliver for getting me started I now have a solution that both supports refactoring and is type safe. It also let me implement INotifyPropertyChanged so it copes with properties being renamed.
It’s usage looks like:
checkBoxCanEdit.Bind(c => c.Checked, person, p => p.UserCanEdit); textBoxName.BindEnabled(person, p => p.UserCanEdit); checkBoxEmployed.BindEnabled(person, p => p.UserCanEdit); trackBarAge.BindEnabled(person, p => p.UserCanEdit); textBoxName.Bind(c => c.Text, person, d => d.Name); checkBoxEmployed.Bind(c => c.Checked, person, d => d.Employed); trackBarAge.Bind(c => c.Value, person, d => d.Age); labelName.BindLabelText(person, p => p.Name); labelEmployed.BindLabelText(person, p => p.Employed); labelAge.BindLabelText(person, p => p.Age);
The person class shows how to implemented INotifyPropertyChanged in a type safe way (or see this answer for a other rather nice way of implementing INotifyPropertyChanged, ActiveSharp - Automatic INotifyPropertyChanged also looks good ):
public class Person : INotifyPropertyChanged { private bool _employed; public bool Employed { get { return _employed; } set { _employed = value; OnPropertyChanged(() => c.Employed); } } // etc private void OnPropertyChanged(Expression<Func<object>> property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(BindingHelper.Name(property))); } } public event PropertyChangedEventHandler PropertyChanged; }
The WinForms binding helper class has the meat in it that makes it all work:
namespace TypeSafeBinding { public static class BindingHelper { private static string GetMemberName(Expression expression) { // The nameof operator was implemented in C# 6.0 with .NET 4.6 // and VS2015 in July 2015. // The following is still valid for C# < 6.0 switch (expression.NodeType) { case ExpressionType.MemberAccess: var memberExpression = (MemberExpression) expression; var supername = GetMemberName(memberExpression.Expression); if (String.IsNullOrEmpty(supername)) return memberExpression.Member.Name; return String.Concat(supername, '.', memberExpression.Member.Name); case ExpressionType.Call: var callExpression = (MethodCallExpression) expression; return callExpression.Method.Name; case ExpressionType.Convert: var unaryExpression = (UnaryExpression) expression; return GetMemberName(unaryExpression.Operand); case ExpressionType.Parameter: case ExpressionType.Constant: //Change return String.Empty; default: throw new ArgumentException("The expression is not a member access or method call expression"); } } public static string Name<T, T2>(Expression<Func<T, T2>> expression) { return GetMemberName(expression.Body); } //NEW public static string Name<T>(Expression<Func<T>> expression) { return GetMemberName(expression.Body); } public static void Bind<TC, TD, TP>(this TC control, Expression<Func<TC, TP>> controlProperty, TD dataSource, Expression<Func<TD, TP>> dataMember) where TC : Control { control.DataBindings.Add(Name(controlProperty), dataSource, Name(dataMember)); } public static void BindLabelText<T>(this Label control, T dataObject, Expression<Func<T, object>> dataMember) { // as this is way one any type of property is ok control.DataBindings.Add("Text", dataObject, Name(dataMember)); } public static void BindEnabled<T>(this Control control, T dataObject, Expression<Func<T, bool>> dataMember) { control.Bind(c => c.Enabled, dataObject, dataMember); } } }
This makes use of a lot of the new stuff in C# 3.5 and shows just what is possible. Now if only we had hygienic macros lisp programmer may stop calling us second class citizens)