views:

1007

answers:

3

I'm developing an application in Silverlight2 and trying to follow the Model-View-ViewModel pattern. I am binding the IsEnabled property on some controls to a boolean property on the ViewModel.

I'm running into problems when those properties are derived from other properties. Let's say I have a Save button that I only want to be enabled when it's possible to save (data has been loaded, and we're currently not busy doing stuff in the database).

So I have a couple of properties like this:

    private bool m_DatabaseBusy;
    public bool DatabaseBusy
    {
        get { return m_DatabaseBusy; }
        set
        {
            if (m_DatabaseBusy != value)
            {
                m_DatabaseBusy = value;
                OnPropertyChanged("DatabaseBusy");
            }
        }
    }

    private bool m_IsLoaded;
    public bool IsLoaded
    {
        get { return m_IsLoaded; }
        set
        {
            if (m_IsLoaded != value)
            {
                m_IsLoaded = value;
                OnPropertyChanged("IsLoaded");
            }
        }
    }

Now what I want to do is this:

public bool CanSave
{
     get { return this.IsLoaded && !this.DatabaseBusy; }
}

But note the lack of property-changed notification.

So the question is: What is a clean way of exposing a single boolean property I can bind to, but is calculated instead of being explicitly set and provides notification so the UI can update correctly?

EDIT: Thanks for the help everyone - I got it going and had a go at making a custom attribute. I'm posting the source here in case anyone's interested. I'm sure it could be done in a cleaner way, so if you see any flaws, add a comment or an answer.

Basically what I did was made an interface that defined a list of key-value pairs to hold what properties depended on other properties:

public interface INotifyDependentPropertyChanged
{
    // key,value = parent_property_name, child_property_name, where child depends on parent.
    List<KeyValuePair<string, string>> DependentPropertyList{get;}
}

I then made the attribute to go on each property:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
public class NotifyDependsOnAttribute : Attribute
{
    public string DependsOn { get; set; }
    public NotifyDependsOnAttribute(string dependsOn)
    {
        this.DependsOn = dependsOn;
    }

    public static void BuildDependentPropertyList(object obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }

        var obj_interface = (obj as INotifyDependentPropertyChanged);

        if (obj_interface == null)
        {
            throw new Exception(string.Format("Type {0} does not implement INotifyDependentPropertyChanged.",obj.GetType().Name));
        }

        obj_interface.DependentPropertyList.Clear();

        // Build the list of dependent properties.
        foreach (var property in obj.GetType().GetProperties())
        {
            // Find all of our attributes (may be multiple).
            var attributeArray = (NotifyDependsOnAttribute[])property.GetCustomAttributes(typeof(NotifyDependsOnAttribute), false);

            foreach (var attribute in attributeArray)
            {
                obj_interface.DependentPropertyList.Add(new KeyValuePair<string, string>(attribute.DependsOn, property.Name));
            }
        }
    }
}

The attribute itself only stores a single string. You can define multiple dependencies per property. The guts of the attribute is in the BuildDependentPropertyList static function. You have to call this in the constructor of your class. (Anyone know if there's a way to do this via a class/constructor attribute?) In my case all this is hidden away in a base class, so in the subclasses you just put the attributes on the properties. Then you modify your OnPropertyChanged equivalent to look for any dependencies. Here's my ViewModel base class as an example:

public class ViewModel : INotifyPropertyChanged, INotifyDependentPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyname)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyname));

            // fire for dependent properties
            foreach (var p in this.DependentPropertyList.Where((x) => x.Key.Equals(propertyname)))
            {
                PropertyChanged(this, new PropertyChangedEventArgs(p.Value));
            }
        }
    }

    private List<KeyValuePair<string, string>> m_DependentPropertyList = new List<KeyValuePair<string, string>>();
    public List<KeyValuePair<string, string>> DependentPropertyList
    {
        get { return m_DependentPropertyList; }
    }

    public ViewModel()
    {
        NotifyDependsOnAttribute.BuildDependentPropertyList(this);
    }
 }

Finally, you set the attributes on the affected properties. I like this way because the derived property holds the properties it depends on, rather than the other way around.

    [NotifyDependsOn("Session")]
    [NotifyDependsOn("DatabaseBusy")]
    public bool SaveEnabled
    {
        get { return !this.Session.IsLocked && !this.DatabaseBusy; }
    }

The big caveat here is that it only works when the other properties are members of the current class. In the example above, if this.Session.IsLocked changes, the notification doesnt get through. The way I get around this is to subscribe to this.Session.NotifyPropertyChanged and fire PropertyChanged for "Session". (Yes, this would result in events firing where they didnt need to)

+2  A: 

You need to add a notification for the CanSave property change everywhere one of the properties it depends changes:

OnPropertyChanged("DatabaseBusy");
OnPropertyChanged("CanSave");

And

OnPropertyChanged("IsEnabled");
OnPropertyChanged("CanSave");
Franci Penov
This would work correctly, but it means that you will be invalidating CanSave more than is necessary, possibly making the rest of the application do more work than is required.
KeithMahoney
What if I dont have access to the set method of the property?
geofftnz
+1  A: 

How about this solution?

private bool _previousCanSave;
private void UpdateCanSave()
{
    if (CanSave != _previousCanSave)
    {
        _previousCanSave = CanSave;
        OnPropertyChanged("CanSave");
    }
}

Then call UpdateCanSave() in the setters of IsLoaded and DatabaseBusy?

If you cannot modify the setters of IsLoaded and DatabaseBusy because they are in different classes, you could try calling UpdateCanSave() in the PropertyChanged event handler for the object defining IsLoaded and DatabaseBusy.

KeithMahoney
+4  A: 

The traditional way to do this is to add an OnPropertyChanged call to each of the properties that might affect your calculated one, like this:

public bool IsLoaded
{
    get { return m_IsLoaded; }
    set
    {
        if (m_IsLoaded != value)
        {
            m_IsLoaded = value;
            OnPropertyChanged("IsLoaded");
            OnPropertyChanged("CanSave");
        }
    }
}

This can get a bit messy (if, for example, your calculation in CanSave changes).

One (cleaner? I don't know) way to get around this would be to override OnPropertyChanged and make the call there:

protected override void OnPropertyChanged(string propertyName)
{
    base.OnPropertyChanged(propertyName);
    if (propertyName == "IsLoaded" /* || propertyName == etc */)
    {
        base.OnPropertyChanged("CanSave");
    }
}
Matt Hamilton
I'm going to go for the OnPropertyChanged override. I wonder if the dependencies could be declared as attributes?
geofftnz
You could set up a static List<string> to keep track of property names that CanSave depends on. Then it'd be a single "if (_canSaveProperties.Contains(propertyName))".
Matt Hamilton
Good idea, thanks!
geofftnz