mosquito87
mosquito87

Reputation: 4440

WPF mvvm property in viewmodel without setter?

I'm dealing with some WPF problems using and sticking to the MVVM pattern.

Most of my properties look like this:

public string Period
{
    get { return _primaryModel.Period; }
    set
    {
        if (_primaryModel.Period != value)
        {
            _primaryModel.Period = value;
            RaisePropertyChanged("Period");
        }
    }
}

This works excellent.

However I also have some properties like this:

public bool EnableConsignor
{
    get
    {
        return (ConsignorViewModel.Id != 0);
    }
}

It doesn't have a setter as the id is changed "automatically" (every time the save of ConsignorViewModel is called. However this leads to the problem that the "system" doesn't know when the bool changes from false to true (as no RaisePropertyChanged is called).

Upvotes: 5

Views: 2568

Answers (2)

Xi Sigma
Xi Sigma

Reputation: 2372

I wrote this a while ago an it has been working great

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class CalculatedProperty : Attribute
{
    private string[] _props;
    public CalculatedProperty(params string[] props)
    {
        this._props = props;
    }

    public string[] Properties
    {
        get
        {
            return _props;
        }
    }
}

The ViewModel base

public class ObservableObject : INotifyPropertyChanged
{
    private static Dictionary<string, Dictionary<string, string[]>> calculatedPropertiesOfTypes = new Dictionary<string, Dictionary<string, string[]>>();

    private readonly bool hasComputedProperties;
    public ObservableObject()
    {
        Type t = GetType();
        if (!calculatedPropertiesOfTypes.ContainsKey(t.FullName))
        {
            var props = t.GetProperties();

            foreach (var pInfo in props)
            {
                var attr = pInfo.GetCustomAttribute<CalculatedProperty>(false);
                if (attr == null)
                    continue;

                if (!calculatedPropertiesOfTypes.ContainsKey(t.FullName))
                {
                    calculatedPropertiesOfTypes[t.FullName] = new Dictionary<string, string[]>();
                }
                calculatedPropertiesOfTypes[t.FullName][pInfo.Name] = attr.Properties;
            }
        }

        if (calculatedPropertiesOfTypes.ContainsKey(t.FullName))
            hasComputedProperties = true;
    }


    public event PropertyChangedEventHandler PropertyChanged;
    public virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));

        if (this.hasComputedProperties)
        {
            //check for any computed properties that depend on this property
            var computedPropNames =
                calculatedPropertiesOfTypes[this.GetType().FullName]
                .Where(kvp => kvp.Value.Contains(propertyName))
                .Select(kvp => kvp.Key);

            if (computedPropNames != null)
                if (!computedPropNames.Any())
                    return;

            //raise property changed for every computed property that is dependant on the property we did just set
            foreach (var computedPropName in computedPropNames)
            {
                //to avoid stackoverflow as a result of infinite recursion if a property depends on itself!
                if (computedPropName == propertyName)
                  throw new InvalidOperationException("A property can't depend on itself");

                OnPropertyChanged(computedPropName);
            }
        }
    }


    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;

        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

Example:

public class ViewModel : ObservableObject
{
    private int _x;
    public int X
    {
        get { return _x; }
        set { SetField(ref _x, value); }
    }

    private int _y;

    public int Y
    {
        get { return _y; }
        set { SetField(ref _y, value); }

    }

    //use the CalculatedProperty annotation for properties that depend on other properties and pass it the prop names that it depends on
    [CalculatedProperty("X", "Y")]
    public int Z
    {
        get { return X * Y; }
    }

    [CalculatedProperty("Z")]
    public int M
    {
        get { return Y * Z; }
    }

}

Note that:

  • it uses reflection only once per type
  • SetField sets the field and raises property changed if there is a new value
  • you don't need to pass property name to SetField as long as you call it within a setter because the [CallerMemberName] does it for you since c# 5.0.
  • if you call SetField outside the setter then you will have to pass it the property name
  • as per my last update you can avoid using SetField by setting the field directly and then calling OnPropertyChanged("PropertyName") and it will raise PropertyChanged for all properties that are dependant on it.
  • in c# 6 you can use the nameof operator to get the property name such as nameof(Property)
  • OnPropertyChanged will call itself recusively if there are computed properties

XAML for testing

<StackPanel>
    <TextBox Text="{Binding X,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
    <TextBox Text="{Binding Y,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
    <TextBlock Text="{Binding Z,Mode=OneWay}"></TextBlock>
    <TextBlock Text="{Binding M,Mode=OneWay}"></TextBlock>
</StackPanel>

Upvotes: 2

BradleyDotNET
BradleyDotNET

Reputation: 61339

For these kinds of properties, you need to just raise PropertyChanged when the dependent data is changed. Something like:

public object ConsignorViewModel
{
   get { return consignorViewModel; }
   set
   {
       consignorViewModel = value;
       RaisePropertyChanged("ConsignorViewModel");
       RaisePropertyChanged("EnableConsignor");
   } 
}

RaisePropertyChanged can be invoked in any method, so just put it after whatever operation that would change the return value of EnableConsignor is performed. The above was just an example.

Upvotes: 7

Related Questions