October 29, 2011

INotifyPropertyChanged and “magic-strings”


WPF contains great things like Bindings and interface INotifyPropertyChanged. This two things allow forget the hell from WinForms and almost all previous UI technologies, where you spend 90% of your time for developing and maintenancing UI code (like updates in UI fields, hide or show some controls etc.) If you still don’t know what is it (hmmm, you develop in WPF and don’t know what is “binding”?), you can read about them, for example, here. And I want talk about problem named “magic-strings”.

How it was

So, how usually properties’ implementations look when you use INotifyPropertyChanged? Something like this:

public class Author
  : INotifyPropertyChanged
{
  #region Properties

  #region FirstName

  private string _FirstName;

  public string FirstName
  {
    get { return _FirstName; }
    set
    {
      if (_FirstName == value)
        return;

      _FirstName = value;
      RaisePropertyChanged("FirstName");
    }
  }

  #endregion

  #endregion

  #region Implementation of INotifyPropertyChanged

  public event PropertyChangedEventHandler PropertyChanged;

  public void RaisePropertyChanged(string propertyName)
  {
    if (PropertyChanged != null)
      PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  }

  #endregion
}
The problem is in highlighted line – there is “magic-string” – “FirstName”. You can make typo error in this string or when you will change property’s name using refactoring – this string will not change. Also, there is yet one place, when this “magic-string” is used – PropertyChanged event’s handler. It looks like this:

public class AuthorViewModel
{
  public AuthorViewModel()
  {
    Author = new Author();
    Author.PropertyChanged += Author_PropertyChanged;
  }

  public Author Author { get; private set; }

  void Author_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  {
    switch (e.PropertyName)
    {
      case "FirstName":
        MessageBox.Show("Author.FirstName was changed");
        break;
    }
  }
}

The problem is the same – you can make typo error or you can change name of this property later. Also, I don’t like how “switch” code looks like. So, what to do?
The answer is – Lambdas!

How it is

I want to warn all optimizators and other perfectionists. Yes, I know that my code below works slower than “magic-strings”. But I prefer more robust code than faster. If your code is fast but it works wrong – do you really need it?
I saw a lot of articles where another guys describe the same decision with RaisePropertyChanged() method, and Prism also uses this approach. But I never saw before usage of the same principle in PropertyChanged event’s handler.
OK, sorry for a lot of words, here is how the same code looks after.

public class Author
  : NotificationObject
{
  #region Properties

  #region FirstName

  private string _FirstName;

  public string FirstName
  {
    get { return _FirstName; }
    set
    {
      if (_FirstName == value)
        return;

      _FirstName = value;
      RaisePropertyChanged(() => FirstName);
    }
  }

  #endregion

  #endregion
}
Here is actually not too much changes. We move implementation of INotifyPropertyChanged to base class NotificationObject and (attention!) remove “magic-string”. So, now we can refactor Author class and do anything you want with FirstName property’s name! And PropertyChanged event’s handler changes to:

void Author_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
  On.PropertyChanged(e, (Author a) => a.FirstName, () => MessageBox.Show("Author.FirstName was changed"));
}
It’s also now simple and robust.
All necessary code you can download by links in the end of this article.

Fast property implementation

It’s terrible to write every time the same code for implementation of properties. So, if you use ReSharper, you can download code template for it, and implement properties with just some keystrokes. All you should do:
  1. Type “pcp” (without quotes) + press Tab
  2. Type property’s type + press Tab
  3. Type property’s name + press Tab
It’s everything you should to do!
You can import that code template by VS main menu –> ReSharper –> Live Templates… –> Import…

Typical use cases

Raise property change notification after changes of another property

For example, class Author has properties FirstName, LastName and FullName, which is equal to FirstName + LastName. When FirstName or LastName is changed, of course you want to notify about FullName changes also. You can do this 2 ways:
1. Add additional code to FirstName’s and LastName’s setters:

public string FirstName
{
  get { return _FirstName; }
  set
  {
    if (_FirstName == value) return;

    _FirstName = value;
    RaisePropertyChanges(() => FirstName);
    RaisePropertyChanges(() => FullName);
  }
}
2. Add additional code to Author.PropertyChanges event’s handler:

public class Author
{
  public Author
  {
    PropertyChanged += Author_PropertyChanges;
  }

  void Author_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  {
    On.PropertyChanged(e, () => FirstName, () => RaisePropertyChanged(() => FullName));
    On.PropertyChanged(e, () => LastName, () => RaisePropertyChanged(() => FullName));
  }
}
Maybe there are some more code, but I think it’s more understandable. All additional notifications and actions on properties’ changes are now in separate place.

Additional action on property’s changes

For example, you want to get from database some additional information after setting ID.

public class Author
{
  public Author
  {
    PropertyChanged += Author_PropertyChanges;
  }

  void Author_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  {
    On.PropertyChanged(e, () => ID, UpdateData);
  }

  void UpdateData()
  {
    // Your code here
  }
}

Additional actions in parent class

public class AuthorViewModel
{
  public Author Author { get; private set; }

  public AuthorViewModel
  {
    Author.PropertyChanged += Author_PropertyChanged;
  }

  void Author_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  {
    On.PropertyChanged<Author>(e, a => a.ID, UpdateData);
    // Or you can use this syntax:
    // On.PropertyChanged(e, (Author a) => a.ID, UpdateData);
  }

  // This method invokes when Author.ID changes
  void UpdateData()
  {
    // Your code here
  }
}

Downloads

No comments:

Post a Comment