Introduction
First of all - the INotifyPropertyChanged
is used for implementation of Observable pattern and it is used for DataBinding in WPF.
I have seen many different implemetations, but I never see any convinient one.
The convinience for me is:
- Thread safety - the bindable property should be updated only from thread in which control binded to viewmodel was created.
- Properties Names - are constants which can be used everywere.
- Presence of tools, helpfull for realization of notifyable properties.
- Possibility to have bulk notifications (when changing of property takes a while and you updating a big view model some times it is better to update all fields and then sequentially notificate view about changes)
So I will share my knowlege, let't go...
Using the code
Let suppose that we have a class:
public class BadExample : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Let's implement a string property MyProperty
:
public String MyProperty
{
get { return _myProperty; }
set
{
_myProperty = value;
OnPropertyChanged("MyProperty");
}
}
private String _myProperty;
Here are the problems:
- Thread safety
- Usage of constant string
- If you will have some difficult logic which will change
_myProperty
field without using property, you should call OnPropetyChanged("MyProperty")
manually
- You have to have simple properties, you unable to have bulk notifications
Sometimes you can see realizations with using constants fields:
public String MyProperty
{
get { return _myProperty; }
set
{
_myProperty = value;
OnPropertyChanged(MyPropertyName);
}
}
private String _myProperty;
public const String MyPropertyName = "MyProperty";
After .NET 4.5 it is possible to use CallerMemberNameAttribute
and write implementation of property like that:
public class BadExample : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
public String MyProperty
{
get { return _myProperty; }
set
{
_myProperty = value;
OnPropertyChanged();
}
}
private String _myProperty;
}
It introduces some convinience but do not solves any problems of advanced coding, which includes multithreading or requirements to notify from another places.
In C# 6.0 it is possible to use nameof(MyProperty)
construction...
Solution
My solution is to implement a base class named Notificator
which will implement some usefull functionality.
Properies realisation will be such:
public String MyProperty
{
get { return _MyProperty; }
set
{
ChangeProperty(
MyProperty_PropertyName,
() => _MyProperty == value,
() => _MyProperty = value);
}
}
private String _MyProperty;
public static readonly String MyProperty_PropertyName = nameof(MyProperty);
Also somebody will say that it is still unconvinient because you should write too much code.
The answer is - the things are hidden in realization of base class, mentioned as Notificator
, and simplicity of code writing is hidden in a codesnippet
(you can read about code snippets in the internet, about how to add and how to use).
Here is the code snippet code: Download nprop.zip
Notificator
public abstract class Notificator : INotifyPropertyChanged
{
private static volatile Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
public static Action<Action> SafeCall = action =>
{
if (_dispatcher.CheckAccess())
{
action();
}
else
{
_dispatcher.Invoke(action);
}
};
protected Notificator()
{
NotifySubscribers = true;
}
public event PropertyChangedEventHandler PropertyChanged;
public Boolean NotifySubscribers { get; set; }
[DebuggerStepThrough]
public void ChangeProperty(
String property,
Func<Boolean> areEqual,
Action setAction,
params String[] additionalPropertyNames)
{
if (areEqual()) return;
setAction();
if (NotifySubscribers && PropertyChanged != null)
{
Raise_PropertyChanged(property);
foreach (var additionalPropertyName in additionalPropertyNames)
{
Raise_PropertyChanged(additionalPropertyName);
}
}
}
private void Raise_PropertyChanged(String property)
{
if (property == null)
{
Debugger.Break();
}
var pc = PropertyChanged;
if (pc == null) return;
SafeCall(() => pc(this, new PropertyChangedEventArgs(property)));
}
[DebuggerStepThrough]
public void RaiseAllPropertiesChanged()
{
var t = GetType();
RaiseAllPropertiesChanged(t);
}
[DebuggerStepThrough]
public void RaiseAllPropertiesChanged(Type t)
{
var fields =
t.GetFields(
BindingFlags.Public |
BindingFlags.Static |
BindingFlags.FlattenHierarchy)
.Where(
f => f.Name.EndsWith("_PropertyName"));
foreach (var fieldInfo in fields)
{
RaiseVirtualPropertyChanged((String)fieldInfo.GetValue(t));
}
}
[DebuggerStepThrough]
public void RaiseVirtualPropertyChanged(params String[] propertyNames)
{
var pc = PropertyChanged;
if (pc == null) return;
foreach (var additionalPropertyName in propertyNames)
{
SafeCall(() => pc(this, new PropertyChangedEventArgs(additionalPropertyName)));
}
}
}
How it is thread safe?
For purposes of thread safetey there are following things:
- Presence of private
_dispatcher
field - it is a global storage of main thread, and for this be able to work you should use Notificator class first time at the beginning of your application from main thread.
SafeCall
function - it just invokes any action from _dispatcher's thread.
How to turn off notifications on the object?
As you can see there is property NotifySubscribers
, wich is responsible for that.
How to implement bulk notification?
You shoul set NotifySubscribers to FALSE, then change any properties you want, then turn NotifySubscribers to TRUE, and then call RaiseAllPropertiesChaged.
It is very easy to write helper function which will take an action in parameter and implement such sequence.
Was it tested?
Yes, I have a project with hundreds viewmodels, and I have no any problems with this, just convinience. So if you will find this usefull please use it and the code become cleaner...