Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aggregate data from child into parent for Binding

This post is completely edited to provide a more solid example and simplified ask

I'm looking to have the results from Child changes be reflected in the Parent, particularly the field NumberOfChildrenWithDegrees. If you check both boxes with bindings of HasUniversityDegree and HasHighSchoolDegree then HasTwoDegrees in the Child View updates. What I would like is the number of Children with HasTwoDegrees be reflected in the Parent VM.

I thought this would just be simple, but it turns out that it doesn't work - checking two boxes changes nothing in the Parent View (the <TextBlock Text="{Binding NumberOfChildrenWithDegrees,Mode=TwoWay,UpdateSourceTrigger=Explicit}"/> area. How can I get this number change to happen real time?

I've created a small sample for Windows Phone that can be plugged directly into a page called TestPage. Just place the XAML in the content grid and the code in the code-behind (VB).

XAML:

    <Grid x:Name="ContentPanel" Background="{StaticResource PhoneChromeBrush}" Grid.Row="1">
        <ListBox x:Name="parentLB" Margin="12,12,12,12" HorizontalContentAlignment="Stretch">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical" Margin="0,12,0,0">
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Parent Name: "/>
                            <TextBlock Text="{Binding Name}"/>
                        </StackPanel>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Number of Children with Degrees: "/>
                            <TextBlock Text="{Binding NumberOfChildrenWithDegrees,Mode=TwoWay,UpdateSourceTrigger=Explicit}"/>
                        </StackPanel>
                        <ListBox Margin="12,0,0,0" x:Name="group2" ItemsSource="{Binding Children,Mode=TwoWay}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <StackPanel>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="Child Name: "/>
                                            <TextBlock Text="{Binding Name}"/>
                                        </StackPanel>
                                            <CheckBox Content="Has High School Degree:" IsChecked="{Binding HasHighSchoolDegree,Mode=TwoWay}"/>
                                            <CheckBox Content="Has University Degree: " IsChecked="{Binding HasUniversityDegree,Mode=TwoWay}"/>
                                            <CheckBox Content="Has Two Degrees? " IsEnabled="False" IsChecked="{Binding HasTwoDegrees}"/>
                                    </StackPanel>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>

Code-behind:

Imports System.Collections.ObjectModel
Imports System.ComponentModel

Partial Public Class TestPage
    Inherits PhoneApplicationPage

    Public Sub New()
        InitializeComponent()
        parentLB.ItemsSource = Grandparent1.ParentGroups
    End Sub
    Public Function Grandparent1() As GrandParent
        Dim ParentGroups As New List(Of Parent)
        ParentGroups.Add(Parent1)
        ParentGroups.Add(Parent2)
        Dim gp As New GrandParent
        gp.ParentGroups = ParentGroups
        Return gp
    End Function
    Public Function Parent2() As Parent
        Dim p As New Parent With {.Name = "Tom"}
        Dim c1 As New Child With {.Name = "Tammy"}
        Dim c2 As New Child With {.Name = "Timmy"}
        Dim children As New List(Of Child)
        children.Add(c1)
        children.Add(c2)
        p.Children = children
        Return p
    End Function

    Public Function Parent1() As Parent
        Dim p As New Parent With {.Name = "Carol"}
        Dim c1 As New Child With {.Name = "Carl"}
        c1.HasHighSchoolDegree = True
        c1.HasUniversityDegree = True
        Dim c2 As New Child With {.Name = "Karla"}
        Dim children As New List(Of Child)
        children.Add(c1)
        children.Add(c2)
        p.Children = children
        Return p
    End Function
End Class

Public Class GrandParent
    Inherits BindableBase
    Public Property ParentGroups As List(Of Parent)
End Class
Public Class Parent
    Inherits BindableBase
    Public Property Name As String

    Private _children As List(Of Child)
    Public Property Children As List(Of Child)
        Get
            Return Me._children
        End Get
        Set(value As List(Of Child))
            Me.SetProperty(Me._children, value)
        End Set
    End Property

    Private _numberOfChildrenWithDegrees As Integer
    Public Property NumberOfChildrenWithDegrees As Integer
        Get
            Return Children.Where(Function(f) f.HasTwoDegrees).Count
        End Get
        Set(value As Integer)
            Me.SetProperty(Me._numberOfChildrenWithDegrees, value)
        End Set
    End Property
End Class
Public Class Child
    Inherits BindableBase
    Public Property Name As String

    Public ReadOnly Property HasTwoDegrees As Boolean
        Get
            Return HasHighSchoolDegree AndAlso HasUniversityDegree
        End Get
    End Property

    Private _hasUniversityDegree As Boolean
    Public Property HasUniversityDegree As Boolean
        Get
            Return Me._hasUniversityDegree
        End Get
        Set(value As Boolean)
            Me.SetProperty(Me._hasUniversityDegree, value)
            OnPropertyChanged("HasTwoDegrees")
        End Set
    End Property

    Private _hasHighSchoolDegree As Boolean
    Public Property HasHighSchoolDegree As Boolean
        Get
            Return Me._hasHighSchoolDegree
        End Get
        Set(value As Boolean)
            Me.SetProperty(Me._hasHighSchoolDegree, value)
            OnPropertyChanged("HasTwoDegrees")
        End Set
    End Property
End Class
Public MustInherit Class BindableBase
    Implements INotifyPropertyChanged
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Protected Function SetProperty(Of T)(ByRef storage As T, value As T,
                                    Optional propertyName As String = Nothing) As Boolean

        If Object.Equals(storage, value) Then Return False

        storage = value
        Me.OnPropertyChanged(propertyName)
        Return True
    End Function
    Protected Sub OnPropertyChanged(Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

End Class
like image 691
Todd Main Avatar asked Feb 05 '14 15:02

Todd Main


2 Answers

Since your child element is the one raising the PropertyChangeEvent and the count is bound to the parent property you need to subscribe to each child's PropertyChangedEvent within the parent. Then you need to raise your own property changed event within the parent with the PropertyName bound to the UI element.

The action you perform when thePropertyChangedEvent is raised is simply calling the OnPropertyChanged() method with the NumberOfChildrenWithDegrees string passed in. If you had a more complex object you may want to perform a case statement within ChildOnPropertyChanged() method based on the PropertyName in the event args. I left a commented example in the code. Here is the full C# code below, I didn't need to change the XAML. Note that I also changed how the list was created and added to in order to subscribe to each child's property changed event upon adding them to the list.

using System.Linq;
using Microsoft.Phone.Controls;
using System.Collections.Generic;
using System.ComponentModel;

namespace PhoneApp1
{



public partial class TestPage : PhoneApplicationPage
{

    public TestPage()
    {
        InitializeComponent();
        this.parentLB.ItemsSource = Grandparent1().ParentGroups;
    }
    public GrandParent Grandparent1()
    {
        List<Parent> ParentGroups = new List<Parent>();
        ParentGroups.Add(Parent1());
        ParentGroups.Add(Parent2());
        GrandParent gp = new GrandParent();
        gp.ParentGroups = ParentGroups;
        return gp;
    }
    public Parent Parent2()
    {
        Parent p = new Parent { Name = "Tom" };
        Child c1 = new Child { Name = "Tammy" };
        Child c2 = new Child { Name = "Timmy" };
        p.AddChild(c1);
        p.AddChild(c2);

        return p;
    }

    public Parent Parent1()
    {
        Parent p = new Parent { Name = "Carol" };
        Child c1 = new Child { Name = "Carl" };
        c1.HasHighSchoolDegree = true;
        c1.HasUniversityDegree = true;
        Child c2 = new Child { Name = "Karla" };
        p.AddChild(c1);
        p.AddChild(c2);

        return p;
    }
}

public class GrandParent : BindableBase
{
    public List<Parent> ParentGroups { get; set; }
}
public class Parent : BindableBase
{
    public string Name { get; set; }



    private List<Child> _children;
    public List<Child> Children
    {
        get { return this._children; }
        set { _children = value; }
    }

    public Parent()
    {
        _children = new List<Child>();
    }

    public void AddChild(Child child)
    {
        child.PropertyChanged += ChildOnPropertyChanged;
        _children.Add(child);
    }

    private void ChildOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
    {
        //if(propertyChangedEventArgs.PropertyName == "HasUniversityDegree");
        OnPropertyChanged("NumberOfChildrenWithDegrees");
    }

    private int _numberOfChildrenWithDegrees;
    public int NumberOfChildrenWithDegrees
    {
        get { return Children.Where(f => f.HasTwoDegrees).Count(); }
        set { _numberOfChildrenWithDegrees = value; }
    }
}
public class Child : BindableBase
{
    public string Name { get; set; }

    public bool HasTwoDegrees
    {
        get { return HasHighSchoolDegree && HasUniversityDegree; }
    }

    private bool _hasUniversityDegree;
    public bool HasUniversityDegree
    {
        get { return this._hasUniversityDegree; }
        set
        {
            _hasUniversityDegree = value;
            OnPropertyChanged("HasTwoDegrees");
        }
    }

    private bool _hasHighSchoolDegree;
    public bool HasHighSchoolDegree
    {
        get { return this._hasHighSchoolDegree; }
        set
        {
            _hasHighSchoolDegree = value;
            OnPropertyChanged("HasTwoDegrees");
        }
    }
}
public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetProperty<T>(ref T storage, T value, string propertyName = null)
    {

        if (object.Equals(storage, value))
            return false;

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }
    protected void OnPropertyChanged(string propertyName = null)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

}
}
like image 86
jmshapland Avatar answered Nov 14 '22 23:11

jmshapland


I've been trying to solve your problem for a little while and I must admit, that I've ended with very similar solution to @jmshapland - the principle is the same: subscribe to child's PropertyChanged. In this situation I was wondering if to write this answer - at the end I've decided to do so, but treat this answer as a bigger comment to @jmshapland's solution. Here goes the code:

Bindable class - making easier to implement INotify:

public abstract class Bindable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String property = null)
    {
        if (object.Equals(storage, value)) return false;
        storage = value;
        this.OnPropertyChanged(property);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string property = null)
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(property));
    }
}

Child class - probably nothing new:

public class Child : Bindable
{
    private string name;
    public string Name
    {
        get { return name; }
        set { SetProperty(ref name, value); }
    }

    public bool HasTwoDegrees { get { return HasHighSchoolDegree && HasUniversityDegree; } }

    private bool hasUniversityDegree;
    public bool HasUniversityDegree
    {
        get { return this.hasUniversityDegree; }
        set
        {
            SetProperty(ref hasUniversityDegree, value);
            OnPropertyChanged("HasTwoDegrees");
        }
    }

    private bool hasHighSchoolDegree;
    public bool HasHighSchoolDegree
    {
        get { return this.hasHighSchoolDegree; }
        set
        {
            SetProperty(ref hasHighSchoolDegree, value);
            OnPropertyChanged("HasTwoDegrees");
        }
    }
}

Parent class - here are some improvements - I subscribe to CollectionChanged event with a method that adds/removes subscription to added/removed items. The subscribed method item_PropertyChanged checks if the property that invoked it, is allowed to update Parent's properties. If so then OnPropertyChanged event is raised for each of defined bindingNames. I think it will be easier if you just look at the code:

public class Parent : Bindable
{       
    private string[] bindingNames;
    private string[] allowedProperties;

    private string name;
    public string Name
    {
        get { return name; }
        set { SetProperty(ref name, value); }
    }

    private ObservableCollection<Child> children = new ObservableCollection<Child>();
    public ObservableCollection<Child> Children
    {
        get { return this.children; }
        set { children = value; }
    }

    public Parent()
    {
        this.children.CollectionChanged += children_CollectionChanged;
        bindingNames = new string[] { "NumberOfChildrenWithDegrees" };
        allowedProperties = new string[] { "HasUniversityDegree", "HasHighSchoolDegree" };
    }

    private void children_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (Object item in e.NewItems)
                if (item is INotifyPropertyChanged)
                    (item as INotifyPropertyChanged).PropertyChanged += item_PropertyChanged;
        if (e.OldItems != null)
            foreach (Object item in e.OldItems)
                (item as INotifyPropertyChanged).PropertyChanged -= item_PropertyChanged;
    }

    private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (bindingNames != null)
            foreach (string item in bindingNames)
                if (allowedProperties.Contains(e.PropertyName))
                    OnPropertyChanged(item);
    }

    public int NumberOfChildrenWithDegrees
    {
        get { return Children.Where(f => f.HasTwoDegrees).Count(); }
    }
}

The MainPage class: (I've not changed the xaml file)

public partial class MainPage : PhoneApplicationPage
{
    ObservableCollection<Parent> parentGroups = new ObservableCollection<Parent>();

    public ObservableCollection<Parent> ParentGroups
    {
        get { return parentGroups; }
        set { parentGroups = value; }
    }

    public MainPage()
    {
        InitializeComponent();
        this.parentLB.DataContext = this;
        FillParents();
    }

    public void FillParents()
    {
        Parent p = new Parent { Name = "Tom" };
        Child c1 = new Child { Name = "Tammy" };
        Child c2 = new Child { Name = "Timmy" };
        p.Children.Add(c1);
        p.Children.Add(c2);
        ParentGroups.Add(p);
        p = new Parent { Name = "Carol" };
        c1 = new Child { Name = "Carl" };
        c1.HasHighSchoolDegree = true;
        c1.HasUniversityDegree = true;
        c2 = new Child { Name = "Karla" };
        p.Children.Add(c1);
        p.Children.Add(c2);
        ParentGroups.Add(p);
    }
}

Maybe it will help a little.

like image 27
Romasz Avatar answered Nov 14 '22 22:11

Romasz