8
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

One thing I don't see you address, is that INotifyPropertyChanged.PropertyChanged has the signature of:

public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);

where object sender should be the owner of the property (typically this). You don't provide the code for ObservableObject, but from your code above it seems that the owner of the property in the BindableProperty property (inheriting ObservableObject) is the BindableProperty and not the owner of the BindableProperty. If my assumptions are correct you break the ownership hierarchy, which may confuse the consumer of the PropertyChanged event, because it doesn't have access to the original source of the event.


if (!EqualityComparer<TValue>.Default.Equals(m_value, value))

Maybe you should provide a constructor that takes a custom EqualityComparer<T> (or IEnqualityComparer<T>as argument. The above seems a little rigid.


Is it because this somewhat pollutes the view model's interface, with IBindableProperty instead of just TValue?

IMO, yes. You pollute your code with a new layer of complexity and obfuscate a quite simple and well established workflow, just because it's irritating to write the same code again and again.

If you are using Visual Studio, you have the opportunity to write and use code snippets to make your coding life easier:

The below creates a property that calls the PropertyChanged event:

    <?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>propnotify</Title>
            <Shortcut>propnotify</Shortcut>
            <Description>Code snippet for property and backing field in a property changed class</Description>
            <Author>Henrik Hansen</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>string</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[private $type$ m_$field$;
    public $type$ $property$
    {
        get { return m_$field$;}
        set 
    { 
      m_$field$ = value;
      OnPropertyChanged(nameof($property$));
    }
    }
    $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

Write "propnotify" in the editor and press TAB and then modify the yellow parts that are likely to change by tabbing between them. You may need to change some pieces to accommodate your style of naming etc.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Yeah, I assumed it was probably the interface that would sink this idea the most. I was actually aware of code snippets to solve this problem, but I've never really bothered to look into creating custom ones myself. But, I've written enough WPF properties to be able to write them in my sleep at this point, so I think I'll finally look into automating them away. \$\endgroup\$
    – HarrisW
    Commented Feb 11, 2020 at 10:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.