C# - 当 child 属性之一发生变化时通知 parent 属性

C# - Notifying the parent property when one of the child properties changes

我有一个 class 和一个 属性 'DesignParameters',更改后会影响另一个 属性,称为 'AxialMomentDataLists'。但是,'DesignParameters' 由一堆其他 'child' 属性组成,这些属性可通过 UI 上的数据网格访问,并且还实现了 属性 更改。如果 child 属性之一发生变化,我还希望 'DesignParameters' 自动更新,这将 in-turn 要求设置新的 'AxialMomentDataLists'。有人对实现此目标的最佳方法有建议吗?

    public class Column : ObservableObject
    {
        private double length;
        private DesignParameters desParameters;

        public DesignParameters DesParameters
        {
            get { return desParameters; }
            set
            {
                desParameters = value;
                RaisePropertyChanged(nameof(DesParameters));
                RaisePropertyChanged(nameof(AxialMomentDataLists));
            }
        }
        public List<AxialMomentDataSet> AxialMomentDataLists
        {
            get { return CalculateAxialMomentLists(ColumnForce, DesParameters); }
            set { }
        }
}

摘自 child class:

public class DesignParameters : ObservableObject
    {
        #region Private variables

        private double phiC;
        private double phiS;
        private int cover;
        private int reinforcementYieldStrength;
        private int concreteStrength;
        private double b;
        private double h;
        private LiveLoadReductionType liveLoadReduction;
        private StirrupType stirrupBar;
        private int numberOfXBars;
        private int numberOfYBars;
        private BarDiameterType longitudinalBarDiameter;
        private double longitudinalReinforcementPercentage;
        List<Bar> rebar;

        #endregion



        public int NumberOfXBars
        {
            get { return numberOfXBars; }
            set
            {
                numberOfXBars = PropertyMethods.SetNumberOfBars("x", value, B, H, Cover, LongitudinalBarDiameter);
                RaisePropertyChanged(nameof(NumberOfXBars));
                RaisePropertyChanged(nameof(Rebar));
                RaisePropertyChanged(nameof(LongitudinalReinforcementPercentage));
                RaisePropertyChanged(nameof(AxialResistance));
                
            }
        }
}

编辑

我创建了一段更简单的代码,试图完成与我在这里大致相同的事情 (https://github.com/dtnaughton/SampleApp)

基本上,我有一个 UI 绑定到 FamilyMember 上的属性,并且可以更改属性 AgeName。在Family(parent)class上,我有一个属性CumulativeFamilyAge其中returns一个方法,基本上总结了一家人的年龄成员(在此示例中,只有 1 个家庭成员,因此它应该与 FamilyMember.Age.

相同

当用户在 UI 上更改 FamilyMember.Age 时,我希望 Family 检测到其中一个 child 属性已更改,因此更新 CumulativeFamilyAge相应地。

我不确定我是否理解正确。 DesignParameters 实现 INotifyPropertyChanged 并且每当它的一个属性发生变化时,您想在 Column class 中为 AxialMomentDataLists [=22] 调用 PropertyChanged =]?

如果是这样,这很容易。每当您为 DesParameters 属性 设置新值时,只需订阅此事件。不要忘记取消订阅旧值的事件。空检查可能是必要的(或者您是否将 C# 8 与可空引用类型一起使用?)

public class Column : ObservableObject
{
    private double length;
    private DesignParameters desParameters;

    public DesignParameters DesParameters
    {
        get { return desParameters; }
        set
        {
            if(desParameters != null)
            {
                desParameters.PropertyChanged -= DesParametersChildPropertyChanged;
            }
            desParameters = value;
            desParameters.PropertyChanged += DesParametersChildPropertyChanged;
            RaisePropertyChanged(nameof(DesParameters));
            RaisePropertyChanged(nameof(AxialMomentDataLists));
        }
    }

    private void DesParametersChildPropertyChanged(object sender, PropertyChangeEventArgs args)
    {
         RaisePropertyChanged(nameof(DesParameters));
         RaisePropertyChanged(nameof(AxialMomentDataLists));
    }

    public List<AxialMomentDataSet> AxialMomentDataLists
   {
        get { return CalculateAxialMomentLists(ColumnForce, DesParameters); }
        set { }
    }
}

For example, if the 'NumberOfXBars' property gets changed (it is exposed on the UI) then I need the parent property 'DesignParameters' on the Column class to recognize one of it's child properties has changed and therefore this must update.

我会按照我的理解来写的。

在列 class 中,您有一个包含 DesignParameters.
实例的 DesParameters 属性 如果此实例的其中一个属性 (NumberOfXBars) 的值已更改,那么您想要更新与此实例关联的所有视图。

这里有一个明显的问题,即违反 INotifyPropertyChanged 接口的约定。根据此合同,当一个值被分配给一个 属性 时,PropertyChanged 事件不会触发,但只有当这个值与前一个值不同时才会触发。
出于这个原因,如果您只是为相同的值引发此事件,那么绑定将不会对此做出反应。

正如我所想,你只是想错了。
您不需要更新实例的绑定。
您需要更新对其任何属性的任何绑定。
为此,接口契约规定创建一个带有空参数的事件。

如果以上是对你问题的正确理解,那么试试这个实现:

    public int NumberOfXBars
    {
        get { return numberOfXBars; }
        set
        {
            numberOfXBars = PropertyMethods.SetNumberOfBars("x", value, B, H, Cover, LongitudinalBarDiameter);
            RaisePropertyChanged(string.Empty);
        }
    }

附加问题后的示例实现:

I have created a more simple piece of code that tries to accomplish approximately the same thing that I am here (https://github.com/dtnaughton/SampleApp)

使用通用 MVVMLight 方法改进了 Person 实体的实现。

using GalaSoft.MvvmLight;

namespace SampleApp.Models
{
    public class Person : ObservableObject
    {
        private string _name;
        private int _age;
        public string Name { get => _name; set => Set(ref _name, value); }
        public int Age { get => _age; set => Set(ref _age, value); }

        public Person()
        {
            Name = "TestName";
            Age = 30;
        }
    }
}

族派生自 ViewModelBase 以便能够观察事件属性的变化。
这样做是为了演示如何观察属性中实体的变化。
还添加了第二个 Person 实体以改进演示示例。

using GalaSoft.MvvmLight;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace SampleApp.Models
{
    public class Family : ViewModelBase
    {
        private Person _familyMember;
        private int _cumulativeFamilyAge;
        private Person _childMember;

        public Person FamilyMember { get => _familyMember; set => Set(ref _familyMember, value); }
        public Person ChildMember { get => _childMember; set => Set(ref _childMember, value); }
        public int CumulativeFamilyAge { get => _cumulativeFamilyAge; private set => Set(ref _cumulativeFamilyAge, value); }

        public Family()
        {
            FamilyMember = new Person() { Name = "Father" };
            ChildMember = new Person() { Name = "Son", Age = 7 };
        }


        public override void RaisePropertyChanged<T>([CallerMemberName] string propertyName = null, T oldValue = default, T newValue = default, bool broadcast = false)
        {
            base.RaisePropertyChanged(propertyName, oldValue, newValue, broadcast);

            if (propertyName == nameof(FamilyMember) || propertyName == nameof(ChildMember))
            {
                if (oldValue is Person oldPerson)
                    oldPerson.PropertyChanged -= OnPersonPropertyChanged;
                if (newValue is Person newPerson)
                    newPerson.PropertyChanged += OnPersonPropertyChanged;

                CalculateCumulativeFamilyAge();
            }

        }

        private void OnPersonPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            CalculateCumulativeFamilyAge();
        }

        public void CalculateCumulativeFamilyAge()
        {
            CumulativeFamilyAge = (FamilyMember?.Age ?? 0)
                                + (ChildMember?.Age ?? 0);
            return;
        }
    }
}

Window 的数据上下文最好在 XAML 中定义 - 这使得 XAML 设计更容易。
如果您需要在 Code Behind 中有一个带有 link 的字段到 ViewModel,您应该从 XAML.

中获取它
using SampleApp.ViewModels;
using System.Windows;

namespace SampleApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly MainViewModel viewModel /*= new MainViewModel()*/;

        public MainWindow()
        {
            InitializeComponent();
            //DataContext = viewModel;
            viewModel = (MainViewModel)DataContext;
        }
    }
}

在 Window 中为 Person 实体添加了一个 DataTemplate 并为两个实体设置了输出。

<Window x:Class="SampleApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewmodels="clr-namespace:SampleApp.ViewModels"
        xmlns:local="clr-namespace:SampleApp.Models"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <viewmodels:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:Person}">
            <Border Margin="5" Background="LightSkyBlue" Padding="5">
                <StackPanel>
                    <TextBox Text="{Binding Name}" Height="30"/>
                    <TextBox Text="{Binding Age}" Height="30"/>
                </StackPanel>
            </Border>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Column="0">
            <ContentPresenter Content="{Binding FamilyModel.FamilyMember}"/>
            <ContentPresenter Content="{Binding FamilyModel.ChildMember}"/>
            <TextBlock Text="{Binding FamilyModel.CumulativeFamilyAge}" Height="30"/>
        </StackPanel>

    </Grid>
</Window>