Xaml Trigger Parent通过Collection中任何子项的特定属性

时间:2016-07-21 09:52:21

标签: c# wpf xaml triggers datatrigger

我在ViewModel中遇到了Child-Parent关系触发器的问题。

考虑以下ViewModel(实现INotifyPropertyChanged)和View:

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

ViewType
 + Visibility: Visibility
 + Header: string

我有一个DataTemplate来绑定2:

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

View的样式:

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

这一切都很美妙。

现在,我想要实现的是,如果从ViewModelTypeInstanceA 所有 ViewModelType.ItemsIsVisible属性设置为false,我希望ViewModelTypeInstanceA对应{{ 1}}将ViewType属性设置为Visibility **。

我已经尝试过DataTriggers,转换器以及所有但我不认为我可以在触发器中使用像AncestorType这样的东西给不可参考的父级吗?似乎无法触发父属性。或许元素可以观察其所有孩子的IsVisisble属性吗?

条件:
1.我宁愿不改变ControlTemplate(它来自差异化) 2.修改ViewModel结构也不是一个选项(兼容性问题)。这意味着我不能在项目创建时维护属性Visibility.Collapsed或者改变收集类型,如Eli建议的那样 我真的更喜欢优雅的解决方案。

**当然,如果可以通过样式设置,那么取决于孩子的ViewType的VisibilityProperty也可以。

2 个答案:

答案 0 :(得分:0)

您可以连接到每个项目的PropertyChanged事件。您还可以继承ObservableCollection并在添加/删除项目时挂钩事件。然后在视图模型中创建一个计算属性并触发其属性更改事件。

另一种选择是使用像ReactiveUI这样的框架。

class ViewModel
{
    public ViewModel()
    {
        Items = new NotificationCollection<Item>();
        Items.ItemPropertyChanged += (o, e) =>
        {
            if (e.PropertyName == nameof(Item.IsVisible))
                OnPropertyChanged(nameof(IsVisible));
        };
    }

    public NotificationCollection<Item> Items { get; }

    public bool IsVisible => Items.All(i => i.IsVisible);
}

如果您无法更改集合的类型,则可以使用CollectionChanged事件:

Items = new ObservableCollection<Item>();
PropertyChangedEventHandler propertyChangedHandler = (o, e) =>
{
    if (e.PropertyName == nameof(Item.IsVisible))
        OnPropertyChanged(nameof(IsVisible));
};

Items.CollectionChanged += (o, e) =>
{
    if (e.OldItems != null)
        foreach (var item in e.OldItems.OfType<INotifyPropertyChanged>())
            item.PropertyChanged -= propertyChangedHandler;
    if (e.NewItems != null)
        foreach (var item in e.NewItems.OfType<INotifyPropertyChanged>())
            item.PropertyChanged += propertyChangedHandler;
};

这是NotificationCollection类:

public class NotificationCollection<T> : ObservableCollection<T>
    where T : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler ItemPropertyChanged;

    protected virtual void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        ItemPropertyChanged?.Invoke(sender, e);
    }

    protected override void ClearItems()
    {
        foreach (var item in Items)
        {
            DisconnectItem(item);
        }
        base.ClearItems();
    }

    protected override void InsertItem(int index, T item)
    {
        base.InsertItem(index, item);
        ConnectItem(item);
    }

    protected override void RemoveItem(int index)
    {
        DisconnectItem(Items[index]);
        base.RemoveItem(index);
    }

    protected override void SetItem(int index, T item)
    {
        DisconnectItem(Items[index]);
        base.SetItem(index, item);
        ConnectItem(item);
    }

    private void ConnectItem(T item)
    {
        if (item != null)
        {
            item.PropertyChanged += OnItemPropertyChanged;
        }
    }

    private void DisconnectItem(T item)
    {
        if (item != null)
        {
            item.PropertyChanged -= OnItemPropertyChanged;
        }
    }
}

答案 1 :(得分:0)

不是最干净,但我认为最好的。我(ab)使用物理父级的DataContext。

  1. 我添加了ViewModelType.HasVisibleChild - 属性和EvaluateHasVisibleChildValue()方法。
  2. 
    internal void EvaluateHasVisibleChild()
    {
      foreach(ViewModelType node in Items)
        // Conditions for ultimate visibility
        if (node.IsVisible && node.HasVisibleChild)
        {
          HasVisibleChild = true;
          return;
        }
    
      HasVisibleChild = false;
    }
    
    1. 我更改了使用转换器的样式绑定,并提供了与Visibility相关的属性作为物理ViewType祖先:
    2. <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. 我写了一个MultiBindingConverter:
      2. 
        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();
          }
        }
        

        实际上我有多个ViewTypes作为祖先,所以我为各种ancestorTypes添加了多个Bindings。我还考虑过为各种ViewTypes添加一个接口,但是nuh-uh。

        N.b。这显然要求每个ViewModelType使用1个ViewType,否则FindAncestorType可能会得到意外的结果。