O interfejsie INotifyPropertyChanged słyszał każdy, gdy napotkał wzorzec MVVM. Niestety podstawowy sposób tworzenia właściwości w klasie ViewModel jest zbyt czasochłonny.
Zacznijmy od przykładu:
public class BasicViewModel : INotifyPropertyChanged
{
private string customerName;
public event PropertyChangedEventHandler PropertyChanged;
public string CustomerName
{
get
{
return customerName;
}
set
{
if (value != customerName)
{
customerName = value;
NotifyPropertyChanged();
}
}
}
public BasicViewModel()
{
CustomerName = "Jan Kowalski";
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Nie dość, że ciało każdej z właściwości jest dość rozległe, to należy implementować interfejs INotifyPropertyChanged w każdej z klas ViewModel osobno. Na szczęście można to uprościć w poniższy sposób:
public class BetterViewModel : ViewModelBase
{
public string CustomerName { get => Get<string>(); set => Set(value); }
public BetterViewModel()
{
CustomerName = "Andrzej Nowak";
}
}
Jak widać, kod jest dużo bardziej kompaktowy.
Mechanizm data-binding’u został przeniesiony do klasy bazowej. Zamiast prywatnych pól dane trzymane są w słowniku, dostęp do niego odbywa się przy użyciu atrybutu CallerMemberName. Rozwiązanie działa dla typów prostych oraz referencyjnych.
public class ViewModelBase : INotifyPropertyChanged
{
private readonly Dictionary<string, object> properties
= new Dictionary<string, object>();
public event PropertyChangedEventHandler PropertyChanged;
protected T Get<T>([CallerMemberName] string name = null)
{
object value = null;
if (properties.TryGetValue(name, out value))
return value == null ? default(T) : (T)value;
return default(T);
}
protected void Set<T>(T value, [CallerMemberName] string name = null)
{
if (Equals(value, Get<T>(name)))
return;
properties[name] = value;
OnPropertyChanged(name);
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
To nie koniec możliwości, jakie daje nam takie rozwiązanie.
Poniżej kilka pomysłów:
- automatyczny trim dla pól typu string
- walidacja (polecam FluentValidation)
- sterowanie pracą przycisku Zapisz na formularzu poprzez zmianę stanu dodatkowego pola IsDirty, jeśli zmianie ulegnie dowolne inne pole