Search code examples
c#wpfxamltriggersdatatrigger

Xaml Trigger Parent via a specific property of any child in Collection


I'm having troubles with triggers for a Child-Parent relation in my ViewModel.

Consider the following ViewModel (implementsINotifyPropertyChanged) and View:

ViewModelType
 + Items: ObservableCollection<ViewModelType>
 + IsVisible: bool
 + Text: string

ViewType
 + Visibility: Visibility
 + Header: string

I have a DataTemplate to bind the 2:

<DataTemplate DataType="{x:Type vm:ViewModelType}">
  <views:ViewType Header="{Binding Text}"/>
</DataTemplate>

And a style for the View:

<Style TargetType="{x:Type views:ViewType}">
  <Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource Bool2Visibility}}"/>
</Style>

This all works beautifully.

Now, what I want to achieve is that if from ViewModelTypeInstanceA all ViewModelType.Items have their IsVisible property set to false, I want ViewModelTypeInstanceA corresponding ViewType to have its Visibility property set to Visibility.Collapsed**.

I have tried DataTriggers, converters and all but I don't think I can use something like AncestorType in triggers to non referable parent? It seems impossible to trigger a parent property. Perhaps it's possible for an element to observe all it's children's IsVisisble properties?

Conditions:
1. I prefer to NOT alter the ControlTemplate (it's from a diff lib).
2. Modifying the ViewModel structure isn't an option either (compatibility issues). That means I cannot maintain a property Parent on item creation or alter the Collection type as Eli suggested.
3. I really prefer an elegant solution.

**Ofcourse depending on the children's ViewType's VisibilityProperty is also OK, if it can be set through styling.


Solution

  • Not the cleanest, but the best that I could think of I guess. I'm (ab)using the physical parent's DataContext.

    1. I added a ViewModelType.HasVisibleChild-property and a EvaluateHasVisibleChildValue() method.
    
    internal void EvaluateHasVisibleChild()
    {
      foreach(ViewModelType node in Items)
        // Conditions for ultimate visibility
        if (node.IsVisible && node.HasVisibleChild)
        {
          HasVisibleChild = true;
          return;
        }
    
      HasVisibleChild = false;
    }
    
    1. I changed the style binding that uses a converter and provides the Visibility-dependent properties both as the physical ViewType ancestor:

     

    <Style TargetType="{x:Type views:ViewType}">
      <Setter.Value>
        <MultiBinding Converter="{StaticResource ConditionalVisibilityConverter}">
          <Binding Path="IsVisible"/>
          <Binding Path="HasVisibleChild"/>
          <Binding Path="." RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type views:ViewType}}"/>
        </MultiBinding>
      </Setter.Value>
    </Style>
    
    1. I wrote a MultiBindingConverter:
    
    public class ConditionalVisibilityConverter : IMultiValueConverter
    {
      public object Convert(object[] values, Type targetType, object paramater, CultureInfo culture)
      {
        // Has no ancestor safety check
        if (values[2] != DependencyProperty.UnsetValue)
          // Sneak in an update of viewmodelparent via physical parent.
          (((values[2] as FrameworkElement).DataContext) as ViewModelType).EvaluateHasVisibleChild();
    
        return (bool)values[0] && (bool)values[1] ? Visibility.Visible : Visibility.Collapsed;
      }
    
      public object[] ConvertBack(object value, Type[] targetTypes, object paramweter, CultureInfo cultuer)
      {
        throw new NotImplementedException();
      }
    }
    

    In practise I have multiple ViewTypes as ancestors, so I added multiple Bindings for various ancestorTypes. I also considered adding an Interface to various the ViewTypes, but nuh-uh.

    N.b. this obviously requires that exactly 1 ViewType is used per ViewModelType, otherwise the FindAncestorType might get unexpected results.