I'm sure anyone who has used WPF is familiar with the irritating boilerplate code surrounding properties, usually of this form.
class TestViewModel : ObservableObject
{
string m_someValue;
decimal m_someOtherValue;
public string SomeValue
{
get => m_someValue;
set
{
if (m_someValue != value)
{
m_someValue = value;
RaisePropertyChanged();
}
}
}
public decimal SomeOtherValue
{
get => m_someOtherValue;
set
{
if (m_someOtherValue != value)
{
m_someOtherValue = value;
RaisePropertyChanged();
}
}
}
}
In this case, ObservableObject
is just a basic convenience class that implements INotifyPropertyChanged
In the interest of getting rid of this junk, I've come up with a small class that wraps the property code, called BindableProperty<TValue>
/// <summary>
/// An object that raises <see cref="INotifyPropertyChanged.PropertyChanged"/> whenever it's value
/// is modified.
/// </summary>
/// <typeparam name="TValue">The type of value that this property holds.</typeparam>
class BindableProperty<TValue> : ObservableObject, IBindableProperty<TValue>
{
/// <summary>
/// An action that is raised when the value changes.
/// </summary>
readonly Action<TValue, TValue> m_onPropertyChangedAction;
TValue m_value;
/// <summary>
/// The value of this property. Raises <see cref="INotifyPropertyChanged.PropertyChanged"/> when modified.
/// </summary>
public TValue Value
{
get => m_value;
set
{
if (!EqualityComparer<TValue>.Default.Equals(m_value, value))
{
TValue previousValue = m_value;
m_value = value;
RaisePropertyChanged();
// Raise the delegate if one has been set.
m_onPropertyChangedAction?.Invoke(previousValue, m_value);
}
}
}
/// <summary>
/// Creates a new bindable property with the default value for the type.
/// </summary>
public BindableProperty() {}
/// <summary>
/// Creates a new bindable property with the given value.
/// </summary>
public BindableProperty(TValue value) : this()
{
Value = value;
}
/// <summary>
/// Creates a new bindable property with the given value, and an action that will be
/// raised when the value changes.
/// </summary>
/// <param name="onPropertyChagnedAction">The action to raise whenever the value changes. The first
/// parameter of this action will be the old value, and the second will be the new value.</param>
/// <exception cref="ArgumentNullException"></exception>
public BindableProperty(TValue value, Action<TValue, TValue> onPropertyChagnedAction) : this(value)
{
m_onPropertyChangedAction = onPropertyChagnedAction ?? throw new ArgumentNullException(nameof(onPropertyChagnedAction));
}
}
With the following interfaces.
/// <summary>
/// An object that raises <see cref="INotifyPropertyChanged.PropertyChanged"/> whenever it's value
/// is modified.
/// </summary>
/// <typeparam name="TValue">The type of value that this property holds.</typeparam>
interface IBindableProperty<TValue> : IBindableReadOnlyProperty<TValue>
{
/// <summary>
/// The value of this property. Raises <see cref="INotifyPropertyChanged.PropertyChanged"/> when modified.
/// </summary>
new TValue Value { set; }
}
/// <summary>
/// A read only variant of <see cref="IBindableProperty{TValue}"/>.
/// </summary>
/// <typeparam name="TValue"></typeparam>
interface IBindableReadOnlyProperty<TValue>
{
TValue Value { get; }
}
Usage
Is can be used in the following ways.
class OtherViewModel
{
IBindableProperty<string> SomeValue { get; } = new BindableProperty<string>("Default value for \"Some value\"");
IBindableProperty<decimal> SomeOtherValue { get; } = new BindableProperty<decimal>();
}
If you need some code to happen after the value changes, then an action can be passed in.
public OtherViewModel()
{
SomeValue = new BindableProperty<string>("Default value for \"Some value\"", OnSomeValueChanged);
}
/// <summary>
/// This will be called when SomeValue.Value is modified.
/// </summary>
void OnSomeValueChanged(string oldValue, string newValue)
{
// TODO: Whatever needs to happen here.
}
Additionally, if for whatever reason you want the value to be changeable by the view, but private to other classes that can reference the view model, then the following can be done.
class OtherViewModel
{
/// <summary>
/// This value can still be modified internally.
/// </summary>
readonly IBindableProperty<string> m_someValue;
/// <summary>
/// Only expose the readonly type, so that other view model's can modify it.
/// </summary>
IBindableReadOnlyProperty<string> SomeValue => m_someValue;
public OtherViewModel()
{
m_someValue = new BindableProperty<string>();
}
}
In this case, the view can still bind to the BindableProperty
backing the interface property and access its set;
property, but other classes will only have access to the get;
through IBindableReadOnlyProperty
.
A bit hacky, perhaps, but no more so than the equivalent using regular properties.
The Question
I've seen plenty of WPF samples, but I've never seen anything like this before, which leads me to wonder why. There's no way I could have been the first to have thought of this, and if this were a good idea you'd think it would have spread, so I want to know if there's some obvious problem with this approach that I'm not seeing.
I haven't actually used it in any projects yet, so perhaps doing so would reveal its flaws, whatever they may be.
Is it because this somewhat pollutes the view model's interface, with IBindableProperty<TValue>
instead of just TValue
?
Is it because everyone else is just using PropertyChanged.Fody
, or some other such package, and so this is redundant?
Please let me know what, if anything, is wrong with this idea.