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

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

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

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

(Это проблема в WinForms, ASP.NET, WPF и, возможно, других системах.)

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

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


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

25.08.2009 15:44:01
Цитата из связанного вопроса: эта проблема теперь решается во время компиляции! Оператор nameof был реализован в C # 6.0 с .NET 4.6 и VS2015 в июле 2015 года. Следующие ответы по-прежнему действительны для C # <6.0. - Майк ( stackoverflow.com/users/772086/mike )
Mads Ravn 22.03.2016 10:05:40
@MadsRavn, это не решает столько, сколько вы надеетесь, так как его нельзя использовать из XAML и не обеспечивает безопасность типов. Тем не менее, он разрешает рефакторинг, когда привязка выполняется из «кода».
Ian Ringrose 22.03.2016 10:59:53
@IanRingrose Справедливо, проблема не будет решена, пока у нас не будет времени безопасности типа компиляции / возможности использовать его из разметки, такой как XAML. Однако моя главная мысль заключалась в том, что решение в принятом ответе (BindingHelper) не следует использовать в C # 6.0 и более поздних версиях, где того же можно достичь с помощью оператора nameof. Ответ теперь отражает это, так что я счастлив :)
Mads Ravn 23.03.2016 09:01:03
См. Ссылку, как обнаружить в XAML нарушенные привязки уже во время компиляции : stackoverflow.com/questions/43208011/…
Rekshino 20.02.2018 11:21:30
7 ОТВЕТОВ

Оператор nameof был реализован в C # 6.0 с .NET 4.6 и VS2015 в июле 2015 года. Следующее все еще действует для C # <6.0

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

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), это весьма полезно (по крайней мере, для меня), но ваш код все еще не безопасен для типов. Вы можете расширить метод Name с помощью аргумента второго типа, который определяет возвращаемое значение функции, что ограничивает тип свойства.

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

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

Наилучшие пожелания

27
22.03.2016 11:04:47
Спасибо за отличную отправную точку, я только что опубликовал ответ, который расширяет вашу работу, чтобы обеспечить безопасность типов.
Ian Ringrose 26.08.2009 11:06:12
РЕШЕНИЕ

Спасибо Оливеру за то, что я начал, и теперь у меня есть решение, которое поддерживает рефакторинг и безопасно для типов. Это также позволило мне реализовать 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 есть мясо, которое заставляет все это работать:

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 может перестать называть нас гражданами второго сорта)

52
23.05.2017 11:55:13
Требуется ли для этого метод OnPropertyChanged быть реализованным для каждого типа? Если это так, это несколько хорошо, но не идеально, и часто метод OnPropertyChanged реализуется в базовом классе и вызывается из всех производных классов.
Davy8 26.08.2009 18:41:07
Дэви, нет причины, по которой метод OnPropertyChanged (и событие) нельзя было просто переместить в базовый класс и сделать защищенным. (Это то, что я ожидал бы сделать в реальной жизни)
Ian Ringrose 26.08.2009 19:25:46
Но из вашего примера кажется, что он основан на параметре типа Expression <Func <Person, object >>, не нужно ли реализовывать метод для каждого типа, чтобы получить параметр типа Expression <Func <Foo, объект >>, выражение <Func <бар, объект >> и т. д.?
Davy8 27.08.2009 17:53:23
Теперь я изменил OnPropertyChanged на OnPropertyChanged (свойство Expression <Func <object >>), что позволит перемещать его в базовый класс.
Ian Ringrose 28.08.2009 10:12:22
Ian Ringrose 13.10.2010 10:46:41

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

Реальная механика может быть немного неприглядной, но она все равно будет безопасна для типов и будет примерно такой же производительности, что и необработанный 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);
        }
    }
}
5
24.04.2010 22:29:37
Хорошая мысль, однако, в большинстве программ это не так сложно, поэтому попробуйте сначала простой способ.
Ian Ringrose 26.04.2010 10:43:11

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

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

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

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

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

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

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

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

3
12.02.2011 20:48:52

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

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

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

Да, Ян, это именно проблемы с привязкой данных на основе именных строк. Вы просили дизайн-шаблон. Я разработал шаблон Type-Safe View Model (TVM), который представляет собой конкретную часть модели View модели Pattern-View-ViewModel (MVVM). Он основан на типобезопасной привязке, аналогичной вашему собственному ответу. Я только что опубликовал решение для WPF:

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

3
4.09.2012 07:51:38
Хорошо, но, похоже, много работы и разрушительный переход от связывания в XAML к связыванию в Code Behind, когда все, что нужно было сделать MSFT, это фактически скомпилировать привязки из XAML. В любом случае он скомпилирован в BAML, поэтому оправданий не так много.
Bruno Brant 26.02.2015 20:19:49

Framework 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 через nuget , который также предоставляет этот атрибут.

25
6.03.2016 07:20:49

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

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

https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension

Разница между связыванием и х: связывание

1
26.11.2019 14:56:21