Xaml 通过集合中任何子项的特定 属性 触发父项
Xaml Trigger Parent via a specific property of any child in Collection
我在使用 ViewModel 中的父子关系触发器时遇到问题。
考虑以下 ViewModel(实现INotifyPropertyChanged
)和视图:
ViewModelType
+ Items: ObservableCollection<ViewModelType>
+ IsVisible: bool
+ Text: string
ViewType
+ Visibility: Visibility
+ Header: string
我有一个数据模板来绑定 2:
<DataTemplate DataType="{x:Type vm:ViewModelType}">
<views:ViewType Header="{Binding Text}"/>
</DataTemplate>
以及视图的样式:
<Style TargetType="{x:Type views:ViewType}">
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource Bool2Visibility}}"/>
</Style>
这一切都很完美。
现在,我想要实现的是,如果从 ViewModelTypeInstanceA all ViewModelType.Items
将它们的 IsVisible
属性 设置为 false
,我希望 ViewModelTypeInstanceA 对应 ViewType
将其 Visibility
属性 设置为 Visibility.Collapsed
**.
我已经尝试过 DataTriggers、转换器和所有但我不认为我可以在触发器中使用像 AncestorType 这样的东西到不可引用的父级?似乎不可能触发父 属性。也许一个元素可以观察到它的所有子元素的 IsVisisble 属性?
条件:
1. 我宁愿不改变 ControlTemplate(它来自差异库)。
2. 修改 ViewModel 结构也不是一个选项(兼容性问题)。这意味着我无法按照 Eli 的建议在项目创建上维护 属性 Parent
或更改 Collection 类型。
3. 我真的更喜欢优雅的解决方案。
**当然依赖children的ViewType的VisibilityProperty也是可以的,如果可以通过styling设置的话
您可以连接到每个项目的 PropertyChanged
事件。您还可以继承 ObservableCollection
并在项目为 added/removed 时挂接事件。然后在视图模型中创建一个计算的 属性 并触发其 属性 changed 事件。
另一种选择是使用像 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
class:
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;
}
}
}
不是最干净的,但我猜是我能想到的最好的。我正在(ab)使用物理父级的 DataContext。
- 我添加了一个
ViewModelType.HasVisibleChild
-属性 和一个 EvaluateHasVisibleChildValue()
方法。
internal void EvaluateHasVisibleChild()
{
foreach(ViewModelType node in Items)
// Conditions for ultimate visibility
if (node.IsVisible && node.HasVisibleChild)
{
HasVisibleChild = true;
return;
}
HasVisibleChild = false;
}
- 我更改了使用转换器并提供可见性相关属性的样式绑定作为物理
ViewType
祖先:
<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>
- 我写了一个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();
}
}
实际上我有多个 ViewType 作为祖先,所以我为各种祖先类型添加了多个绑定。我还考虑过为各种 ViewTypes 添加一个接口,但是 nuh-uh.
N.b。这显然要求每个 ViewModelType 使用 1 个 ViewType,否则 FindAncestorType 可能会得到意想不到的结果。
我在使用 ViewModel 中的父子关系触发器时遇到问题。
考虑以下 ViewModel(实现INotifyPropertyChanged
)和视图:
ViewModelType
+ Items: ObservableCollection<ViewModelType>
+ IsVisible: bool
+ Text: string
ViewType
+ Visibility: Visibility
+ Header: string
我有一个数据模板来绑定 2:
<DataTemplate DataType="{x:Type vm:ViewModelType}">
<views:ViewType Header="{Binding Text}"/>
</DataTemplate>
以及视图的样式:
<Style TargetType="{x:Type views:ViewType}">
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource Bool2Visibility}}"/>
</Style>
这一切都很完美。
现在,我想要实现的是,如果从 ViewModelTypeInstanceA all ViewModelType.Items
将它们的 IsVisible
属性 设置为 false
,我希望 ViewModelTypeInstanceA 对应 ViewType
将其 Visibility
属性 设置为 Visibility.Collapsed
**.
我已经尝试过 DataTriggers、转换器和所有但我不认为我可以在触发器中使用像 AncestorType 这样的东西到不可引用的父级?似乎不可能触发父 属性。也许一个元素可以观察到它的所有子元素的 IsVisisble 属性?
条件:
1. 我宁愿不改变 ControlTemplate(它来自差异库)。
2. 修改 ViewModel 结构也不是一个选项(兼容性问题)。这意味着我无法按照 Eli 的建议在项目创建上维护 属性 Parent
或更改 Collection 类型。
3. 我真的更喜欢优雅的解决方案。
**当然依赖children的ViewType的VisibilityProperty也是可以的,如果可以通过styling设置的话
您可以连接到每个项目的 PropertyChanged
事件。您还可以继承 ObservableCollection
并在项目为 added/removed 时挂接事件。然后在视图模型中创建一个计算的 属性 并触发其 属性 changed 事件。
另一种选择是使用像 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
class:
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;
}
}
}
不是最干净的,但我猜是我能想到的最好的。我正在(ab)使用物理父级的 DataContext。
- 我添加了一个
ViewModelType.HasVisibleChild
-属性 和一个EvaluateHasVisibleChildValue()
方法。
internal void EvaluateHasVisibleChild()
{
foreach(ViewModelType node in Items)
// Conditions for ultimate visibility
if (node.IsVisible && node.HasVisibleChild)
{
HasVisibleChild = true;
return;
}
HasVisibleChild = false;
}
- 我更改了使用转换器并提供可见性相关属性的样式绑定作为物理
ViewType
祖先:
<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>
- 我写了一个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();
}
}
实际上我有多个 ViewType 作为祖先,所以我为各种祖先类型添加了多个绑定。我还考虑过为各种 ViewTypes 添加一个接口,但是 nuh-uh.
N.b。这显然要求每个 ViewModelType 使用 1 个 ViewType,否则 FindAncestorType 可能会得到意想不到的结果。