在ViewModel中使用WPF MVVM属性但不带Setter?

5
我正在处理一些使用MVVM模式的WPF问题。
我的大多数属性看起来像这样:
public string Period
{
    get { return _primaryModel.Period; }
    set
    {
        if (_primaryModel.Period != value)
        {
            _primaryModel.Period = value;
            RaisePropertyChanged("Period");
        }
    }
}

这很出色。然而,我还有一些像这样的属性:
public bool EnableConsignor
{
    get
    {
        return (ConsignorViewModel.Id != 0);
    }
}

它没有setter,因为id是“自动”更改的(每次调用ConsignorViewModel的保存时)。然而,这会导致一个问题,即“系统”不知道bool何时从false变为true(因为没有调用RaisePropertyChanged)。
2个回答

7
对于这些属性,当依赖数据发生变化时,只需触发 PropertyChanged。例如:
public object ConsignorViewModel
{
   get { return consignorViewModel; }
   set
   {
       consignorViewModel = value;
       RaisePropertyChanged("ConsignorViewModel");
       RaisePropertyChanged("EnableConsignor");
   } 
}

RaisePropertyChanged 可以在任何方法中调用,因此只需将它放在更改 EnableConsignor 返回值的操作后面即可。上面仅是一个示例。


8
而且,是的,每个人在这样做时都会感到肮脏。 - user1228
1
这并不算太糟糕,而且是迄今为止微软团队的首选做法(C# 6.0之前)。重构时可能会有些困难。使用C# 6.0后,可以使用RaisePropertyChanged(nameof(EnableConsignor)),这是重构安全的。如果您无法使用C# 6.0并且没有性能关键代码,则仍然可以使用自定义实现的RaisePropertyChanged,该实现接受表达式并将其用作获取属性名称的字符串。 - Tseng
@Tseng 知道它不脏并不能改变我每次这样做时的感觉很脏;) - BradleyDotNET
是的,但如果有大量绑定需要快速更新,那么就没有性能的替代方案。但幸运的是,Visual Studio 2015 RTM已经很接近了 :P - Tseng

2

我之前写过这个程序,一直运行得很好。

[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;
        }
    }
}

视图模型基类
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;
    }
}

例子:

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; }
    }

}

注意:
- 它仅对每种类型使用一次反射 - `SetField` 设置字段并在有新值时引发属性更改 - 只要在 setter 内部调用它,就不需要向 `SetField` 传递属性名称,因为自 C# 5.0 以来 `[CallerMemberName]` 会自动帮您完成。 - 如果在 setter 之外调用 `SetField`,则必须传递属性名称。 - 根据我的最新更新,您可以通过直接设置字段,然后调用 `OnPropertyChanged("PropertyName")` 来避免使用 `SetField`,它将为所有依赖它的属性引发 PropertyChanged 事件。 - 在 C# 6 中,您可以使用 nameof 运算符获取属性名称,例如 `nameof(Property)` - 如果存在计算属性,则 `OnPropertyChanged` 将递归调用自身
测试用的 XAML
<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>

@mosquito87 对其进行了一些改进,现在调用 OnPropertyChanged("PropertyName") 将会为所有相关属性引发属性更改,而以前只有调用 SetField 才能这样做。请使用我提供的 XAML 测试上述示例。 - Xi Sigma
这是一件有趣的事情,但是这行代码无法编译:pInfo.GetCustomAttribute<CalculatedProperty>(false)。也许你想使用 PropertyInfo.GetCustomAttributes(Type, bool) - Xaruth
@Xaruth FirstOrDefault! - Xi Sigma
@Xaruth 没错,同时删除 [CallerMemberName] 并将属性名称传递给 SetField(ref _x,value,"X") - Xi Sigma
1
[CallerMemberName]并不是问题,我因为Thomas Levesque在4.0中使用它:http://www.thomaslevesque.fr/2012/06/ - Xaruth
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接