Основы и примеры MVVM. Yet Another Property Changed Notification

  • Вы работаете с WPF? Вы слышали про MVVM?
  • Тогда мы идем к вам!!!

Итак, для тех, кто еще не в курсе, что такое «MVVM» (аббревиатура Model-View-ViewModel) — хочу вас обрадовать: вы обязательно об этом скоро узнаете 😉

Для самых любознательных рекомендую отличный материал от наших коллег из Infragistics, публикующихся на страничках MSDN: https://msdn.microsoft.com/ru-ru/magazine/dd419663.aspx

В современном мире мы, проектируя интерфейсы пользователя на WPF, сталкиваемся с такими вещами, как привязка данных (Data Binding). Для того, чтобы этот механизм использовался в полном объеме, привязываемый объект должен реализовывать механизм оповещений (INotifyPropertyChanged) об изменении его свойств, чтобы binding — среда могла правильно определить изменение свойства и выполнить обновление GUI. Вот как обычно выглядит привязка в коде на XAML:

<TextBox Text="{Binding Path=SomeData}" ..../>

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

public class SomeClass
    {
        public string SomeData
        {
            get;
            set;
        }
    }

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

using System.ComponentModel;

//........

  public class SomeClass : INotifyPropertyChanged
    {
        string _someData;
        public string SomeData
        {
            get { return _someData; }
            set
            {
                _someData = value;
                NotifyPropertyChanged("SomeData");
            }
        }

        private void NotifyPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }

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

Кроме того, бывает так, что для огромного количества свойств в классе (а это и есть плохой дизайн!!!!) очень проблематично выделять дополнительное поле-контейнер данных (в нашем примере — string _someData), а также оповещение об изменении свойства NotifyPropertyChanged(«SomeData») долго писать для каждого свойства в отдельности.

Было бы очень здорово этот механизм автоматизировать! Отлично, сегодня мы попробуем это осуществить.

Отдельная благодарность — Денису aka Storm — за идею.

Итак, вернемся к исходному классу:

public class SomeClass
    {
        public string SomeData
        {
            get;
            set;
        }
    }

Есть несколько способов авто реализации данного механизма: инжектирование интерфейсов (при помощи генерации proxy-объекта, этот способ мы рассмотрим позже, возможно в другой статье), создание run-time wrapper-а, реализующего интерфейс и имеющего все свойства оригинального объекта, создание CustomTypeDescriptor.

Мы будем рассмотрим последний способ, как наиболее подходящий для наших целей:

using System.ComponentModel;

//....................

    public class ChangeNotifier : CustomTypeDescriptor, INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }

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

public override PropertyDescriptorCollection GetProperties()
{
return base.GetProperties();
}

Опять же, ничего полезного пока нет, потому что нам все еще необходим механизм, оповещающий об изменении свойств, причем каждого свойства. То есть событие, срабатывающее при вызове метода SetValue каждого PropertyDescriptor’а в данной коллекции свойств. Для этого нужно реализовать класс-наследник от PropertyDescriptor.

using System.ComponentModel;

//....................

internal class ChangeNotificationPropertyDescriptor : PropertyDescriptor
    {
        PropertyDescriptor wrapped;
        object target;

        public event Action<Object, Object, PropertyDescriptor> PropertyChanged = delegate { };

        public ChangeNotificationPropertyDescriptor(PropertyDescriptor descr, object target)
            : base(descr.Name, null)
        {
            this.target = target;
            this.wrapped = descr;
        }

        public override bool CanResetValue(object component)
        {
            return wrapped.CanResetValue(component);
        }

        public override Type ComponentType
        {
            get { return wrapped.ComponentType; }
        }


        public override bool IsReadOnly
        {
            get { return wrapped.IsReadOnly; }
        }

        public override Type PropertyType
        {
            get { return wrapped.PropertyType; }
        }

        public override object GetValue(object component)
        {
            return wrapped.GetValue(target);
        }

        public override void ResetValue(object component)
        {
            wrapped.ResetValue(target);
        }

        public override void SetValue(object component, object value)
        {
            wrapped.SetValue(target, value);
            PropertyChanged(target, value, wrapped);
        }

        public override bool ShouldSerializeValue(object component)
        {
            return wrapped.ShouldSerializeValue(target);
        }

        public override void AddValueChanged(object component, EventHandler handler)
        {
            wrapped.AddValueChanged(target, handler);
        }
    }

Обратите внимание, что мы сделали обертку (wrapped) над оригинальным PropertyDescriptor’ом, потому что базовый абстрактный класс не имеет реализации метода SetValue.

Теперь мы можем использовать наш ChangeNotificationPropertyDescriptor в классе ChangeNotifier, кроме того сделаем наш класс generic, чтобы им было удобнее пользоваться:

using System.ComponentModel;

//....................

 public class ChangeNotifier<T> : CustomTypeDescriptor, INotifyPropertyChanged
    {
        List<ChangeNotificationPropertyDescriptor> allProps;
        PropertyDescriptorCollection pdc;

        T _target;
        public T Target { get { return _target; } }

        public ChangeNotifier(T target)
        {
            this._target = target;
            allProps = TypeDescriptor.GetProperties(typeof(T))
                    .OfType<PropertyDescriptor>()
                    .Select(x => new ChangeNotificationPropertyDescriptor(x, _target))
                    .ToList();

            allProps.ForEach(x => x.PropertyChanged += OnPropertyChanged);
            pdc = new PropertyDescriptorCollection(allProps.ToArray());
        }

        void OnPropertyChanged(object component, object value, PropertyDescriptor pd)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(pd.Name));
        }

        public override PropertyDescriptorCollection GetProperties()
        {
            return pdc;
        }


        public void SetProperty(string name, object value)
        {
            var prop = allProps.Where(x => x.Name == name).FirstOrDefault();
            if (prop != null)
            {
                prop.SetValue(this, value);
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        #endregion
    }

Обратите внимание на объявление T Target — это экземпляр оригинального класса (модель). T — оригинальный класс.

Чтобы создать экземпляр такого дескриптора для нашего SomeClass выполним следующие действия:

ChangeNotifier<SomeClass> viewModel = new ChangeNotifier<SomeClass>(new SomeClass
            {
                SomeData = "test"
            });

Теперь экземпляр ChangeNotifier можно, например, присвоить свойству DataContext нескольким элементам TextBox, для которых назначен Binding на свойство SomeData. Это свойство при изменении будет вызывать оповещение PropertyChanged и данные обновятся в обоих текстовых полях.

Код примера:

Window1.xaml:

<Window x:Class="WpfApplication1.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <StackPanel x:Name="panel">
            <TextBlock Text="Enter some data:"/>
            <TextBox Text="{Binding SomeData, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Margin="0,20,0,0" Text="{Binding SomeData}"/>
        </StackPanel>
    </Grid>
</Window>

Window1.xaml.cs:

 public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            panel.DataContext = new ChangeNotifier<SomeClass>(new SomeClass
            {
                SomeData = "test"
            });
        }
    }

Ну вот и все на сегодня!

Спасибо всем большое за то что дочитали до этого места 😉

Рейтинг
( Пока оценок нет )
Загрузка ...