- Вы работаете с 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" }); } }
Ну вот и все на сегодня!
Спасибо всем большое за то что дочитали до этого места 😉