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








