簡體   English   中英

使用MVVM更新WPF中的派生屬性

[英]Update derived property in WPF with MVVM

可以使用什么模式來確保在連接多個源時在UI中更新屬性。

例如,我有一個窗口標題的字符串屬性。 它顯示應用程序名稱(const字符串),程序集版本(只讀字符串)以及根據用戶輸入加載的類型的實例屬性。

有沒有辦法讓title屬性訂閱實例屬性,以便在加載實例時標題自動更新?

現在加載配方時,它會更新title屬性。 但我想扭轉這一點,以便食譜不知道標題。 它只是廣播它被加載,然后任何需要對正在加載的配方做出反應的東西都會孤立地處理事件。

什么設計模式適合這個?

我在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屬性。 默認情況下,出於性能原因,它不會檢查級聯中的循環,因此它依賴於您不創建它們。 您可以選擇將PreventLoops上的PreventLoops設置為true ,並確保僅為每個屬性引發PropertyChanged一次。

不確定這是否理想,但我的解決方案是處理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語句。 另一種方法是使用信使。

    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中,它仍然會收到通知,並且它們不會緊密耦合。 任何需要處理配方更改的東西都可以注冊消息並接收通知,而無需處理每個屬性更改事件的巨型方法。

但我想扭轉這一點,以便食譜不知道標題。 它只是廣播它被加載,然后任何需要對正在加載的配方做出反應的東西都會孤立地處理事件。

聽起來像史蒂文的Cleary計算屬性是你需要的: https//github.com/StephenCleary/CalculatedProperties

我已經在https://stackoverflow.com/a/41444180/15536​​41上詳細回答了類似的問題

這個圖書館很神奇。 事實上,我會推薦它用於任何 MVVM項目,無論是新的還是舊的,逐步采用計算出的特性以及享受即時效益是微不足道的。

灰是正確的,你需要綁定工作。 快速谷歌搜索有很多結果,但這個似乎涵蓋了你需要的:

傑里尼克松博客文章

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM