使用MVVM在WPF中更新派生属性

3
什么设计模式可以确保在UI中连接多个数据源时更新属性?
例如,我有一个窗口标题的字符串属性。它由应用程序名称(const字符串)、程序集版本(只读字符串)和基于用户输入加载的类型的实例属性组成。
有没有办法使标题属性订阅实例属性,以便当实例被加载时,标题自动更新?
目前,当配方被加载时,它会更新标题属性。但我希望反过来,这样配方就不知道标题。它只是广播它已经加载,然后需要对加载的配方做出反应的任何内容都将独立处理事件。
哪种设计模式适合这种情况?

2
你听说过绑定吗?INotifyPropertyChanged?它们一起可以让“标题属性订阅实例属性,因此当实例被加载时,标题会自动更新”的效果。 - ASh
希望你不介意,我稍微编辑了你的问题标题,以更好地符合我们的指南,并更贴近你实际的问题。 - Bradley Uffner
3个回答

1

我在我的MVVM库中使用以下类来允许属性更改向相关属性级联。如果您认为它对您有用,请随意使用:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace AgentOctal.WpfLib
{
    public class PropertyChangeCascade<T> where T : ObservableObject
    {

        public PropertyChangeCascade(ObservableObject target)
        {
            Target = target;

            Target.PropertyChanged += PropertyChangedHandler;
            _cascadeInfo = new Dictionary<string, List<string>>();
        }

        public ObservableObject Target { get; }
        public bool PreventLoops { get; set; } = false;

        private Dictionary<string, List<string>> _cascadeInfo;

        public PropertyChangeCascade<T> AddCascade(string sourceProperty,
                                                   List<string> targetProperties)
        {
            List<string> cascadeList = null;

            if (!_cascadeInfo.TryGetValue(sourceProperty, out cascadeList))
            {
                cascadeList = new List<string>();
                _cascadeInfo.Add(sourceProperty, cascadeList);
            }

            cascadeList.AddRange(targetProperties);

            return this;
        }

        public PropertyChangeCascade<T> AddCascade(Expression<Func<T, object>> sourceProperty,
                                                   Expression<Func<T, object>> targetProperties)
        {
            string sourceName = null;
            var lambda = (LambdaExpression)sourceProperty;

            if (lambda.Body is MemberExpression expressionS)
            {
                sourceName = expressionS.Member.Name;
            }
            else if (lambda.Body is UnaryExpression unaryExpression)
            {
                sourceName = ((MemberExpression)unaryExpression.Operand).Member.Name;
            }
            else
            {
                throw new ArgumentException("sourceProperty must be a single property", nameof(sourceProperty));
            }

            var targetNames = new List<string>();
            lambda = (LambdaExpression)targetProperties;

            if (lambda.Body is MemberExpression expression)
            {
                targetNames.Add(expression.Member.Name);
            }
            else if (lambda.Body is UnaryExpression unaryExpression)
            {
                targetNames.Add(((MemberExpression)unaryExpression.Operand).Member.Name);
            }
            else if (lambda.Body.NodeType == ExpressionType.New)
            {
                var newExp = (NewExpression)lambda.Body;
                foreach (var exp in newExp.Arguments.Select(argument => argument as MemberExpression))
                {
                    if (exp != null)
                    {
                        var mExp = exp;
                        targetNames.Add(mExp.Member.Name);
                    }
                    else
                    {
                        throw new ArgumentException("Syntax Error: targetProperties has to be an expression " +
                                                    "that returns a new object containing a list of " +
                                                    "properties, e.g.: s => new { s.Property1, s.Property2 }");
                    }
                }
            }
            else
            {
                throw new ArgumentException("Syntax Error: targetProperties has to be an expression " +
                                            "that returns a new object containing a list of " +
                                            "properties, e.g.: s => new { s.Property1, s.Property2 }");
            }

            return AddCascade(sourceName, targetNames);
        }

        public void Detach()
        {
            Target.PropertyChanged -= PropertyChangedHandler;
        }

        private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
        {
            List<string> cascadeList = null;

            if (_cascadeInfo.TryGetValue(e.PropertyName, out cascadeList))
            {
                if (PreventLoops)
                {
                    var cascaded = new HashSet<string>();
                    cascadeList.ForEach(cascadeTo =>
                    {
                        if (!cascaded.Contains(cascadeTo))
                        {
                            cascaded.Add(cascadeTo);
                            Target.RaisePropertyChanged(cascadeTo);
                        }
                    });
                }
                else
                {
                    cascadeList.ForEach(cascadeTo =>
                    {
                        Target.RaisePropertyChanged(cascadeTo);
                    });
                }
            }
        }
    }
}

ObservableObject 是实现 INotifyPropertyChanged 接口的基类。您应该可以很容易地替换为自己的基类。

您可以像这样使用它:

class CascadingPropertyVM : ViewModel
{
    public CascadingPropertyVM()
    {
        new PropertyChangeCascade<CascadingPropertyVM>(this)
            .AddCascade(s => s.Name,
            t => new { t.DoubleName, t.TripleName });
    }

    private string _name;
    public string Name
    {
        get => _name;
        set => SetValue(ref _name, value);
    }

    public string DoubleName => $"{Name} {Name}";
    public string TripleName => $"{Name} {Name} {Name}";
}

构造函数中的代码将Name属性的更改与DoubleNameTripleName属性的级联变化相连接。为了性能方面的考虑,它默认不会检查级联中是否存在循环,因此需要您避免创建这种情况。您可以选择在级联上设置PreventLoopstrue,这样它就会确保每个属性只触发一次PropertyChanged

0

但我想反过来,让食谱不知道标题。它只是广播说它已经加载完毕,然后需要对加载食谱做出反应的任何东西都会独立地处理该事件。

听起来像是您需要 Steven 的 Cleary 计算属性:https://github.com/StephenCleary/CalculatedProperties

我已经在 https://dev59.com/4mIk5IYBdhLWcg3wG6o6#41444180 中详细回答了类似的问题。

这个库很神奇。事实上,我会推荐它用于任何MVVM项目,无论是新的还是旧的,采用逐步计算属性是微不足道的,并且可以享受即时的好处。


0

不确定这是否理想,但我的解决方案是处理MVVMLight提供的属性更改事件。

    private Model.Recipe _recipe;
    public Model.Recipe Recipe
    {
        get { return _recipe; }
        set { Set(ref _recipe, value); }
    }

    public string MyProperty
    {
        get { return "Test " + Recipe.MyProperty; }
    }

    public MainViewModel()
    {
        PropertyChanged += MainViewModel_PropertyChanged;
        Recipe = new Model.Recipe();
    }

    private void MainViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case "Recipe": RaisePropertyChanged("MyProperty"); break;
        }
    }

不太喜欢MainViewModel_PropertyChanged变成一个处理所有更改的大量switch语句。另一种替代方法是使用messenger。

    private Model.Recipe _recipe;
    public Model.Recipe Recipe
    {
        get { return _recipe; }
        set { if (Set(ref _recipe, value)) { Messenger.Default.Send(value, "NewRecipe"); } }
    }

    public string MyProperty
    {
        get { return "Test " + Recipe.MyProperty; }
    }

    public MainViewModel()
    {
        Messenger.Default.Register<Model.Recipe>(this, "NewRecipe", NewRecipe);
        Recipe = new Model.Recipe();
    }

    private void NewRecipe(Recipe obj)
    {
        RaisePropertyChanged("MyProperty");
    }

这种方法的好处是,如果MyProperty在不同的ViewModel中,它仍然会接收到通知,并且它们不会紧密耦合。任何需要处理食谱更改的内容都可以注册该消息并接收通知,而无需处理每个属性更改事件的大型方法。

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