Как сделать тип привязки данных безопасным и поддерживать рефакторинг


когда я хочу привязать элемент управления к свойству моего объекта, я должен указать имя свойства как строку. Это не очень хорошо, потому что:

  1. если свойство удалено или переименован, я не получаю компилятор предупреждающий.
  2. если переименовать свойство с помощью инструмента рефакторинга, это скорее всего привязки данных не будет усовершенствованный.
  3. Я не получаю ошибку, пока время выполнения, если тип свойства неверно, например, привязка целого числа к дата селектор.

есть ли дизайн-шаблон, который обходит это, но все еще имеет простоту использования привязки данных?

(это проблема в WinForm, Asp.net и WPF и, скорее всего, много других систем)

теперь я нашел "обходные пути для оператора nameof () в C#: typesafe databinding", который также имеет хорошую отправную точку для решения.

Если вы готовы использовать постпроцессор после компиляции кода, notifypropertyweaver стоит посмотреть.


кто-нибудь знает хорошее решение для WPF, когда привязки выполняются в XML, а не в C#?

7 69

7 ответов:

благодаря Оливеру для начала у меня теперь есть решение, которое поддерживает рефакторинг и является безопасным для типа. Это также позволило мне реализовать INotifyPropertyChanged, чтобы он справлялся с переименованными свойствами.

это использование выглядит так:

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

класс person показывает, как реализовать INotifyPropertyChanged безопасным для типа способом (или посмотреть этот ответ для другого довольно приятного способа реализации INotifyPropertyChanged,ActiveSharp - Автоматический 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;
}

в классе WinForms binding helper есть мясо, которое заставляет все это работать:

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);
        }
    }
}

Это использует много нового материала в C# 3.5 и показывает только то, что возможно. Вот если бы у нас было гигиенические макросы программист lisp может перестать называть нас гражданами второго класса)

оператор nameof был реализован в C# 6.0 с .NET 4.6 и VS2015 в июле 2015 года. По-прежнему действует для C#

чтобы избежать строк, которые содержат имена свойств, я написал простой класс, используя деревья выражений, чтобы вернуть имя члена:

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);
    }
}

вы можете использовать этот класс следующим образом. Хотя вы можете использовать его только в коде (так что не в XAML), это довольно полезно (по крайней мере для меня), но ваш код по-прежнему не является типобезопасным. Вы могли бы продлить имя метода с аргументом второго типа, который определяет возвращаемое значение функции, которое будет ограничивать тип свойства.

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

до сих пор я не нашел ничего, что решает проблему безопасности типов привязки данных.

С Наилучшими Пожеланиями

фреймворк 4.5 предоставляет нам CallerMemberNameAttribute, что делает передачу имени свойства в виде строки ненужной:

private string m_myProperty;
public string MyProperty
{
    get { return m_myProperty; }
    set
    {
        m_myProperty = value;
        OnPropertyChanged();
    }
}

private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
    // ... do stuff here ...
}

Если вы работаете на Framework 4.0 с KB2468871 установлен, вы можете установить Microsoft BCL Compatibility Pack через nuget, который также содержит этот атрибут.

этот блог статья поднимает некоторые вопросы об эффективности такого подхода. Вы можете улучшить эти недостатки, преобразовав выражение в строку как часть какой-то статической инициализации.

фактическая механика может быть немного неприглядной, но она все равно будет типобезопасной и примерно равна производительности raw INotifyPropertyChanged.

что-то вроде этого:

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);
        }
    }
}

один из способов получить обратную связь, если ваши привязки нарушены, - это создать DataTemplate и объявить его тип данных типом ViewModel, к которому он привязывается, например, если у вас есть PersonView и PersonViewModel, вы сделаете следующее:

  1. объявите DataTemplate с типом данных = PersonViewModel и ключом (например, PersonTemplate)

  2. вырезать все PersonView xaml и вставить его в шаблон данных (который в идеале может быть просто в верхней части персонификация.

3a. создайте ContentControl и установите ContentTemplate = PersonTemplate и свяжите его содержимое с PersonViewModel.

3В. Другим вариантом является, чтобы не дать ключ к DataTemplate и не установить ContentTemplate для ContentControl. В этом случае WPF выяснит, какой DataTemplate использовать, так как он знает, к какому типу объекта вы привязываетесь. Он будет искать вверх по дереву и найти свой DataTemplate и так как он соответствует типу привязки, он будет автоматически применять его в качестве ContentTemplate.

в конечном итоге вы получаете по существу тот же вид, что и раньше, но поскольку вы сопоставили DataTemplate с базовым типом данных, такие инструменты, как Resharper, могут дать вам обратную связь (через цветовые идентификаторы - Resharper-Options-Settings-Color Identifiers), чтобы ваши привязки были сломаны или нет.

вы все равно не получите предупреждения компилятора, но можете визуально проверить наличие сломанных Привязок, что лучше, чем необходимость проверьте назад и вперед между вашим видом и viewmodel.

еще одно преимущество этой дополнительной информации, которую вы даете, заключается в том, что она также может быть использована при переименовании рефакторингов. Насколько я помню, Resharper может автоматически переименовывать привязки на типизированных DataTemplates при изменении имени свойства базового ViewModel и наоборот.

1.Если свойство удалено или переименовано, я не получаю предупреждение компилятора.

2.Если переименовать свойство с помощью инструмента рефакторинга, скорее всего, привязка данных не будет обновлена.

3.Я не получаю ошибку до времени выполнения, если тип свойства неверен,например, привязка целого числа к выбору даты.

да, Ян, это точно проблемы с привязкой данных, управляемых строкой имени. Вы просили Дизайн-Шаблон. Я конструированный тип-безопасный вид модели (ТВМ) шаблон, который является оседаний модели представления части модель-представление-модель представления (MVVM) паттерн. Он основан на типобезопасной привязке, аналогичной вашему собственному ответу. Я только что опубликовал решение для WPF:

http://www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM

x:bind (также называемый "скомпилированные привязки данных") для XAML (Универсальное приложение) в windows 10 и windows phone 10 может решить эту проблему, см. https://channel9.msdn.com/Events/Build/2015/3-635

Я не могу найти он-лайн документы для него, но не приложил много усилий, так как это то, что я не буду использовать в течение некоторого времени. Однако этот ответ должен быть полезным указателем на других людей.