Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Specify Full Binding Path from within nested ItemsControls

I'm having a problem with Nested controls with ItemSources when the outermost control's DataContext changes. The inner controls appear to update to reflect the new DataContext, but its like there is some "Ghost" binding that is still tied to the old DataContext.

I suspect that having Nested controls that have DataTemplates prevents in inner control's bindings from updating when the outer control's DataContext is changed. I've read somewhere that only the binding only responds to PropertyChanged events that are raised from object that are expictly defined in the PATH.

My question is: How do you fully define a binding PATH from within nexted controls with ItemsSources? In my case:

<DataGrid name="OuterGrid" ItemsSource={Binding SelectedSchool.Classes}"> 
   <ItemsControl ItemsSource={Binding Students}">
      <ComboBox SelectedItem={Binding Grade}" />
   </ItemsControl>
</DataGrid>

I would like fully specify the inner ComboBox's SeletedItem PATH, something like the following, but I need it to be bound to the specific item within the collection (not just the one at index 0).

<ComboBox SelectedItem="{Binding ElementName=OuterGrid, 
     Path=DataContext.SelectedSchool.Classes[0].Students[0].Grade}" />

I have a more detailed example of my problem below, I'm not able to post actual code or describe the ACTUAL objects that I am working with (security reasons), so I've tried to describe it in the easiest way to understand.


MODEL:

I have a fairly complicated Biz object that has a collection of other objects. Items in the collection also have collections within them.

  • Schools have many classes
  • Classes have many students
  • Each student has a letter grade for the class.
  • The list of possible letter grades is diffrent for each school.

Every class (including my ViewModel) implements INotifyPropertyChanged, and each collection is an ObservableCollection.


VIEWMODEL:

My ViewModel has the following propeties:

  • An ObservableCollection of Schools... (AllSchools).
  • A SelectedSchool
  • A boolean (IsEditing)
  • An ObservableCollection of possible Grades (which gets updated when IsEditing changes, and is based on the Selected School).

The important thing to note here is that diffrent schools may have diffrent possible Grades (i.e one may have A+, A, and A- while another only has A).


XAML:

I have a Datagrid bound to my ViewModel's AllSchools collection and SelectedSchool property of my ViewModel. When the user double-clicks a row, an event-handler opens an "edit panel" for the selected school by changing the ViewModel's IsEditing property (the edit panel's Visibily is bound to IsEditing property). Inside the edit panel I have a Datagrid (Bound to the collection of Classes for the selected School), Inside the Datagrid I have a TemplatedColumn with an ItemsControl (Bound to the collection of the current Class's Students). For each student there is a ComboBox for the student's grade in the class. The ItemsSource for the ComboBox is the ViewModel's PossibleGrades collection.


THE PROBLEM:

The problem is that when the SelectedSchool changes, any student in the previously SelectedSchool with a letter grade that does not exist for the newly-SelectedSchool, suddenly has their letter grade set to null (because the ItemsSource for the ComboBox no longer has the grade).

Visually, everything seems to work fine. The edit panel correctly shows the properties for the selected school, and gets updated when the SelectedSchool property changes. But if I re-open the edit panel for the the fist school, none of the comboboxes have values selected anymore because they were all set to null when I selected the second School.

Its like the old ComboBoxes still have their Bindings hooked-up, even though they no longer show up on the screen. But if only affects the previously SelectedSchool (not the one before it).

like image 557
NuclearProgrammer Avatar asked Sep 16 '15 16:09

NuclearProgrammer


2 Answers

Its like the old ComboBoxes still have their Bindings hooked-up

You are getting warm....

but its like there is some "Ghost" binding that is still tied to the old DataContext.

More like zombie, or really orphan. Let me explain.

At the end of the day a binding is just the xaml compiler reflecting off of a named instance reference and if it applies also looks out for messages from InotifyPropertyChange. Keep that in mind, just a single reference point.

Now we know this data is hierarchical but bindings, like logic, are a cruel Mistress; it does not care. Let us look at the top level binding target of your example:

 <DataGrid name="OuterGrid" ItemsSource={Binding SelectedSchool.Classes}">   

The problem is that when the SelectedSchool changes, any student in the previously SelectedSchool with a letter grade that does not exist for the newly-SelectedSchool,

The school changed, but you are not binding to the School but a former reference of SelectedSchool.Classes, the sub object. Hence the upper changes don't trickle down and the reference is actually still valid and has not changed. But visually you have changed the comboboxes...which has affected the old data.


I recommend you look at the bindings, remove the xxxx.yyyyy and only focus on providing a xxxx or yyyy when an expect hierarchical change occurs; then implement a system where both notify properties get changed and notified at the same time; keeping in mind the appropriate xaml binding to the toplevel and direct sub-levels.

So maybe create a wrapper which implements INotifyPropertyChange which identifies the current, in your faux example, school and also the sub references and when the top changes, the wrapper is smart enought to change the sub references to match the top change and do a cascading notify in the top level setter:

 class MyWrapper : INotifyPropertyChange
 {
   public TheXXX XXXX
   {
      get { return _xxxx; }
      set
          {
             _xxxx = value;
             NotifyChange("XXXX");
             _yyyy = _XXXX.YYYY;
             NotifyChange("YYYY");
            _zzzz = _XXXX.ZZZZ;
             NotifyChange("ZZZZ");
             ...
          }

    ...
  }
like image 168
ΩmegaMan Avatar answered Oct 23 '22 22:10

ΩmegaMan


Thanks to @OmegaMan for a description of what was happening behind the scenes with the binding.

I basically solved it by creating an interface that cascades PropertyChanged events.

public interface ICascadePropertyChanged: INotifyPropertyChanged
{
    void CascadePropertyChanged();
}

I then modified my ModelBase and CollectionBase classes to implement said interface by using Refection to recursively invoke CascadePropertyChanged() on sub-properties.

public class ModelCollection<M>  : ObservableCollection<M>, 
    ICascadePropertyChanged where M: ModelBase
{
    ...
    public void CascadePropertyChanged()
    {
        foreach (M m in this)
        {
             if (m != null)
             {
                 m.CascadePropertyChanged();
             }
        }
    }
}

public abstract class ModelBase: ICascadePropertyChanged
{
    ...
    public void CascadePropertyChanged()
    {
      var properties = this.GetType().GetProperties()
          .Where( p => HasInterface(p.PropertyType, typeof(ICascadePropertyChanged));

      // Cascade the call to each sub-property.
      foreach (PropertyInfo pi in properties)
      {
        ICascadePropertyChanged obj = (ICascadePropertyChanged)pi.GetValue(this);
        if (obj  != null)
        {
            obj.CascadePropertyChanged();
        }
      }
      RaisePropertyChanged();
   }
}

I had to retype this from memory, so please excuse the typos.

like image 2
NuclearProgrammer Avatar answered Oct 24 '22 00:10

NuclearProgrammer