Click here to Skip to main content
15,888,461 members
Articles / Desktop Programming / WPF

Observable : Automatically Notify Property Owner When a Property is Changed

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
5 Jun 2010LGPL32 min read 56.2K   12   1
Reduce the property declaration code to one line, yet still usable directly.

Introduction

Ivan Krivyakov suggested an Observable<T>, when declared as property in a class, and the property is changed, it will raise a PropertyChanged event automatically, thus any WPF control that links to the Observable<T> will update the value.

I found it useful because a developer no longer needs to write getter, setter code and notifyChange code for each property, which can reduce a lot of copy & paste work. So I created the version 2 of Observable<T>. It works, but it has a limitation, as Ivan Krivyakov said:

It is nice indeed, but the users of this will have this pesky “Value” always getting in the way, won’t they?

C#
{Binding MyModel.MyProperty.Value}
// C#
if (ThisProperty.Value > ThatProperty.Value)
{
return A.Value + B.Value * C.Value;
}

That's the reason for the more complicated version 3.

How to Use?

You can set the value using both its Observable<T>.Value property or Observable<T> itself.

C#
public class Person : ModelBase
{
   public Observable<string> LastName { get { return getVar<string>("LastName"); } }
   public Observable<int> Age { get 
	{ return getVar<int>("Age"); } set { setVar<int>("Age", value); } }
}
...
Person p = new Person();
private void Button_Click(object sender, RoutedEventArgs e)
{
   p.LastName.Value += "QuickZip"; //Set using Obsersable<T>'s Value property
   p.Age += 1; //Set directly!
}

XAML

XML
<TextBlock Text="{Binding LastName}" /> <!-- Both way works -->
<TextBlock Text="{Binding Age.Value}" />

How it Works?

In version 2, when Observable<T>.Value is changed, it will trigger Observable<T>’s NotifyChanged() method, thus notify the listener, but as it doesn’t affect its parent (ModelBase), so if a WPF Control is bound to the Observable<T> directly instead of its Value property, it cannot be notified for any changes.

So how does version3 support that?

C#
public Observable<int> Age { get { return getVar<int>("Age"); } 
	set { setVar<int>("Age", value); } }
protected void setVar<T>(string name, Observable<T> value)
{
    ...
    variableDic.Add(name, new Observable<T>(new Action(() => 
			{ NotifyPropertyChanged(name); })));
    ((Observable<T>)variableDic[name]).Value = value.Value;
}

First, setter now can be implemented, like Age in this sample, when set it calls setVar() method, which updates Age.Value. As you see, the new Observable<T> now includes a method to callback, so when its Value is changed, it will call both its and its owner’s NotifyPropertyChanged() method.

But that's not sufficient to make it usable directly, nobody wants to call the following just to set a new value.

C#
Age = new Observable<int>(null) { Value = 11 }

So for XAML access, a TypeConverter is written to “Extract” Observable<T>.Value property.

C#
public class ObservableConverter : TypeConverter
{
  public override object ConvertTo(ITypeDescriptorContext context, 
	CultureInfo culture, object value, Type destinationType)
  {
    object valueValue = value.GetType().GetProperty("Value").GetValue(value, null);
    return base.ConvertTo(context, culture, valueValue, destinationType);
  }
}

For setting from CS code, I overrode a number of operators, including the assignment (implicit, + and -) operator. I used Jon Skeet’s implementation to perform Addition and Subtraction operations to generic operator, if you need you can implement other operators, you can use a similar way to do it, more information about operator can be found in MSDN.

C#
public static implicit operator Observable<T>(T value)
{
    return new Observable<T>(null) { Value = value };
}

public static Observable<T> operator +(Observable<T> value1, Observable<T> value2)
{
    return new Observable<T>(null) { Value = Tools.Add(value1.Value, value2.Value) };
}

Implementation

C#
public class ModelBase : INotifyPropertyChanged
{
    private Dictionary<string, object> variableDic = new Dictionary<string, object>();

    protected Observable<T> getVar<T>(string name) //GetVariable
    {
        if (!variableDic.ContainsKey(name))
            variableDic.Add(name, new Observable<T>
		(new Action(() => { NotifyPropertyChanged(name); })));
        return (Observable<T>)variableDic[name];
    }

    protected void setVar<T>(string name, Observable<T> value)
    {
        if (variableDic.ContainsKey(name))
            variableDic.Remove(name);
        variableDic.Add(name, new Observable<T>(new Action(() => 
				{ NotifyPropertyChanged(name); })));
        ((Observable<T>)variableDic[name]).Value = value.Value;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged(string property)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(property));
    }
}

public class Person : ModelBase
{
    public Observable<string> LastName { get { return getVar<string>("LastName"); } }
    public Observable<int> Age { get { return getVar<int>("Age"); } 
		set { setVar<int>("Age", value); } }
}
public class ObservableConverter : TypeConverter
{
    public override object ConvertTo(ITypeDescriptorContext context, 
		CultureInfo culture, object value, Type destinationType)
    {
        object valueValue = value.GetType().GetProperty("Value").GetValue(value, null);
        return base.ConvertTo(context, culture, valueValue, destinationType);
    }
}

public static class Tools
{
    //http://www.yoda.arachsys.com/csharp/genericoperators.html
    public static T Add<T>(T a, T b)
    {
        // declare the parameters
        ParameterExpression paramA = 
		System.Linq.Expressions.Expression.Parameter(typeof(T), "a"),
            paramB = System.Linq.Expressions.Expression.Parameter(typeof(T), "b");
        // add the parameters together
        BinaryExpression body = System.Linq.Expressions.Expression.Add(paramA, paramB);
        // compile it
        Func<T, T, T> add = System.Linq.Expressions.Expression.Lambda<Func<T, T, T>>
				(body, paramA, paramB).Compile();
        // call it
        return add(a, b);
    }

    public static T Subtract<T>(T a, T b)
    {
        // declare the parameters
        ParameterExpression paramA = 
		System.Linq.Expressions.Expression.Parameter(typeof(T), "a"),
            paramB = System.Linq.Expressions.Expression.Parameter(typeof(T), "b");
        // add the parameters together
        BinaryExpression body = System.Linq.Expressions.Expression.Subtract
				(paramA, paramB);
        // compile it
        Func<T, T, T> subtract = System.Linq.Expressions.Expression.Lambda
				<Func<T, T, T>>(body, paramA, paramB).Compile();
        // call it
        return subtract(a, b);
    }
}

[TypeConverter(typeof(ObservableConverter))]
public class Observable<T> : INotifyPropertyChanged
{
    T _value;
    Action _updateAction;

    public Observable(Action updateAction)
    {
        _updateAction = updateAction;
    }

    public T Value
    {
        get { return _value; }
        set
        {
            if (value == null || !value.Equals(_value))
            {
                _value = value;
                NotifyPropertyChanged("Value");
                if (_updateAction != null)
                    _updateAction.Invoke();
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

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

    public static implicit operator Observable<T>(T value)
    {
        return new Observable<T>(null) { Value = value };
    }

    public static Observable<T> operator +(Observable<T> value1, Observable<T> value2)
    {
        return new Observable<T>(null) { Value = Tools.Add(value1.Value,value2.Value) };
    }

    public static Observable<T> operator -(Observable<T> value1, Observable<T> value2)
    {
        return new Observable<T>(null) 
	{ Value = Tools.Subtract(value1.Value, value2.Value) };
    }
}
}

This article has been posted on . You can find a list of my articles here.


License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Founder
Hong Kong Hong Kong

Comments and Discussions

 
QuestionIs this approach supported by TypeDescriptor.GetProperties? Pin
tonyt6-Jun-10 18:46
tonyt6-Jun-10 18:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.