简体   繁体   中英

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

I have a class with a property 'DesignParameters' that upon changing, would affect another property called 'AxialMomentDataLists'. However, the 'DesignParameters' is comprised of a bunch of other 'child' properties that are accessible through a datagrid on the UI and also implement property changed. If one of the child properties changes, I also want 'DesignParameters' to automatically update, which would in-turn call for a new 'AxialMomentDataLists' to be set. Does anyone have advice on the best method to achieve this?

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

Excerpt from 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));
                
            }
        }
}

EDIT

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 )

Basically, I had a UI that binds to properties on the FamilyMember and the properties Age and Name can be changed. On the Family (parent) class, I have a property CumulativeFamilyAge which returns a method that basically sums up the age of the family member (in this example, there is only 1 family member and therefore it should read the same as the FamilyMember.Age .

When the user changes FamilyMember.Age on the UI, I want Family to detect that one of it's child properties has changed, and therefore update CumulativeFamilyAge accordingly.

I am not sure if I unterstood you correctly. DesignParameters implements INotifyPropertyChanged and whenever one of its properties changes, you want to invoke PropertyChanged in your Column class for the AxialMomentDataLists property?

If so, this is quite easy. Just subscribe to this event whenever you set a new value to your DesParameters property. Don't forget to unsubscribe the event from the old value. A null check might be necessary (or do you use C# 8 with nullable reference types?)

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.

I'll write as I understand it.

In the Column class, you have a DesParameters property that contains an instance of DesignParameters .
If the value of one of the properties (NumberOfXBars) of this instance has changed, then you want to update all views associated with this instance.

Here you have an obvious problem with breaking the contract of the INotifyPropertyChanged interface. According to this contract, the PropertyChanged event does not fire when a value is assigned to a property, but only if this value is different from the previous one.
For this reason, if you just raise this event for the same value, then the bindings will not react to this.

As I think, you are simply not thinking correctly.
You don't need to update the bindings to the instance.
You need to update any bindings to any of its properties.
For this, the interface contract provides for the creation of an event with an empty argument.

If the above is a correct understanding of your problem, then try this implementation:

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

Example implementation after additional question:

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 )

Improved implementation of Person entity using generic MVVMLight methods.

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

Family is derived from ViewModelBase to be able to watch event properties change.
This is done to demonstrate how to observe changes in entities in properties.
Also added a second Person entity to improve the demo example.

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

The Data Context of the Window is best defined in XAML - this makes XAML design much easier.
If you need to have a field in Code Behind with a link to the ViewModel, you should get it from 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;
        }
    }
}

Added a DataTemplate for the Person entity in the Window and set the output for both entities.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM