views:

2316

answers:

4

When I wish to bind a control to a property of my object, I have to provide the name of the property as a string. This is not very good because:

  1. If the property is removed or renamed, I don’t get a compiler warning.
  2. If a rename the property with a refactoring tool, it is likely the data binding will not be updated.
  3. I don’t get an error until runtime if the type of the property is wrong, e.g. binding an integer to a date chooser.

Is there a design-pattern that gets round this, but still has the ease of use of data-binding?

(This is a problem in WinForm, Asp.net and WPF and most likely lots of other systems)

I have now found "workarounds for nameof() operator in C#: typesafe databinding" that also has a good starting point for a solution.


Anyone knows of a good solution for WPF when the bindings are done in XML rather then C#?

+11  A: 

To avoid strings which contain property names, I've written a simple class using expression trees to return the name of the member:

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class Member
{
    private static string GetMemberName(Expression expression)
    {
        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:
                return String.Empty;

            default:
                throw new ArgumentException("The expression is not a member access or method call expression");
        }
    }

    public static string Name<T>(Expression<Func<T, object>> expression)
    {
        return GetMemberName(expression.Body);
    }

    public static string Name<T>(Expression<Action<T>> expression)
    {
        return GetMemberName(expression.Body);
    }
}

You can use this class as follows. Even though you can use it only in code (so not in XAML), it is quite helpful (at least for me), but your code is still not typesafe. You could extend the method Name with a second type argument which defines the return value of the function, which would constrain the type of the property.

var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"

Until now I haven't found anything which solves the databinding typesafety issue.

Best Regards

Oliver Hanappi
Thanks for the great starting point, I have just posted an answer that extends your work to give typesafety.
Ian Ringrose
+12  A: 

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 ):

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 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)
        {
            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)

Ian Ringrose
Does this require that the OnPropertyChanged method be implemented for each type? If so, it's somewhat nice, but not ideal, and often times the OnPropertyChanged method is implemented in a base class and called from all derivative classes.
Davy8
Davy, there is no reason why OnPropertyChanged method (and event) could not just be moved to a base class and make protected. (That what I would expect to do in real life)
Ian Ringrose
But from your example, it seems like it relies on the parameter being of type Expression<Func<Person, object>>, wouldn't the method need to be implemented for each type to take a param of type Expression<Func<Foo, object>>, Expression<Func<Bar, object>>, etc?
Davy8
I have now changed OnPropertyChanged to OnPropertyChanged(Expression<Func<object>> property) that will allow it to be moved into the base class.
Ian Ringrose
see also http://stackoverflow.com/questions/527602/automatically-inotifypropertychanged/527840#527840 for another way to implement OnPropertyChanged - (I was trying to make Databinding type safe, the OnPropertyChanged was just a side line using the same method, I rather like the way the other answer uses a extension method on PropertyChangedEventHandler)
Ian Ringrose
What Control object has such property as DataBindings? I checked MSDN, there is no such property.
macias
@macias, see http://msdn.microsoft.com/en-us/library/system.windows.forms.control.databindings.aspx
Ian Ringrose
:-) It is WinForms, I assumed it was WPF. Thank you for clarification.
macias
A: 

Ian,

I think it's going to take a bit more than that to stop calling you second class citizens ;-)

Regards, Mark

Mark Dalgarno
+2  A: 

This blog article raises some good questions about the performance of this approach. You could improve upon those shortcomings by converting the expression to a string as part of some kind of static initialization.

The actual mechanics might be a little unsightly, but it would still be type-safe, and approximately equal performance to the raw INotifyPropertyChanged.

Something kind of like this:

public class DummyViewModel : ViewModelBase
{
    private class DummyViewModelPropertyInfo
    {
        internal readonly string Dummy;

        internal DummyViewModelPropertyInfo(DummyViewModel model)
        {
            Dummy = BindingHelper.Name(() => model.Dummy);
        }
    }

    private static DummyViewModelPropertyInfo _propertyInfo;
    private DummyViewModelPropertyInfo PropertyInfo
    {
        get { return _propertyInfo ?? (_propertyInfo = new DummyViewModelPropertyInfo(this)); }
    }

    private string _dummyProperty;
    public string Dummy
    {
        get
        {
            return this._dummyProperty;
        }
        set
        {
            this._dummyProperty = value;
            OnPropertyChanged(PropertyInfo.Dummy);
        }
    }
}
nedruod
good point, however in most software it is not lickly to be a problem in reallife, so try the simple way first.
Ian Ringrose