根据 CheckComboBox 中的选择动态 hide/show 属性
Dynamically hide/show properties based on the selection in a CheckComboBox
在 WPF 中,我希望有这样一个视图模型,在 PropertyGrid
中它应该显示一个 CheckComboBox
使我能够动态地 show/hide 其他属性(基于选择)。
我用以下 属性 的内容填充 CheckComboBox
:
// A collection used as the data source for the CheckComboBox.
[RefreshProperties(RefreshProperties.Repaint)]
public ObservableCollection<TriggerTypeItem> TriggerTypes { get; } = new ObservableCollection<TriggerTypeItem>();
其中 TriggerTypeItem
实现了 INotifyPropertyChanged
接口。
但是,似乎对 TriggerTypeItem
元素的更改不会导致 TriggerTypes
属性被视为已修改,因此 - Browsable
属性的动态更改未反映在属性 网格。
(SetBrowsableAttribute()
函数工作正常,您可以通过切换 ShouldShow
属性 的复选框看到。)
我应该怎么做才能达到预期的效果?
MainWindow.xaml
<Window x:Class="WpfPlayground.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="300">
<Grid>
<xctk:PropertyGrid x:Name="PropertyGridControl">
<xctk:PropertyGrid.EditorDefinitions>
<xctk:EditorTemplateDefinition TargetProperties="TriggerTypes">
<xctk:EditorTemplateDefinition.EditingTemplate>
<DataTemplate>
<xctk:CheckComboBox ItemsSource="{Binding Instance.TriggerTypes}"
DisplayMemberPath="TriggerType"
SelectedMemberPath="Selected" />
</DataTemplate>
</xctk:EditorTemplateDefinition.EditingTemplate>
</xctk:EditorTemplateDefinition>
</xctk:PropertyGrid.EditorDefinitions>
</xctk:PropertyGrid>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfPlayground
{
public partial class MainWindow : Window
{
private readonly EventViewModel eventViewModel = new EventViewModel();
public MainWindow()
{
InitializeComponent();
PropertyGridControl.SelectedObject = eventViewModel;
}
}
public abstract class PropertyChangedBase : Component, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyOfPropertyChange([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public enum TriggerType
{
Timer,
}
public class EventViewModel : PropertyChangedBase
{
private bool _shouldShow;
private readonly Dictionary<TriggerType, string> _triggerViewModels = new Dictionary<TriggerType, string>()
{
{ TriggerType.Timer, nameof(ToogleProperty) }
};
// A "regular" checkbox for toogling.
[RefreshProperties(RefreshProperties.Repaint)]
public bool ShouldShow
{
get { return _shouldShow; }
set
{
TypeDescriptor.GetProperties(this)[nameof(ToogleProperty)].SetBrowsableAttribute(value);
_shouldShow = value;
NotifyOfPropertyChange(nameof(ShouldShow));
}
}
// A collection used as the data source for the CheckComboBox.
[RefreshProperties(RefreshProperties.Repaint)]
public ObservableCollection<TriggerTypeItem> TriggerTypes { get; } = new ObservableCollection<TriggerTypeItem>();
// The property to be toogled.
[ReadOnly(true)]
public string ToogleProperty { get; set; } = "(Toggle me!)";
public EventViewModel()
{
ShouldShow = true;
foreach (TriggerType val in Enum.GetValues(typeof(TriggerType)))
{
TriggerTypes.Add(new TriggerTypeItem(triggerType: val, eventModel: this) { Selected = false });
}
}
/// <summary>
/// Dynamically sets the <c>Browsable</c> attribute.
/// </summary>
public void UpdatePropertyGridTriggers(TriggerType triggerType, bool newBrowsableState)
{
if (_triggerViewModels.TryGetValue(triggerType, out string propertyName))
{
TypeDescriptor.GetProperties(this)[propertyName].SetBrowsableAttribute(newBrowsableState);
NotifyOfPropertyChange(nameof(propertyName));
}
}
}
/// <summary>
/// An item in the CheckCoboBox.
/// </summary>
public class TriggerTypeItem : PropertyChangedBase
{
private TriggerType _triggerType;
private bool _selected;
public EventViewModel EventModel { get; private set; }
public TriggerType TriggerType
{
get { return _triggerType; }
set
{
_triggerType = value;
NotifyOfPropertyChange(nameof(TriggerType));
}
}
public bool Selected
{
get { return _selected; }
set
{
if (_selected != value)
{
_selected = value;
EventModel?.UpdatePropertyGridTriggers(TriggerType, value);
NotifyOfPropertyChange(nameof(Selected));
}
}
}
public TriggerTypeItem(TriggerType triggerType, EventViewModel eventModel)
{
EventModel = eventModel;
TriggerType = triggerType;
}
}
public static class PropertyDescriptorExtensions
{
/// See: http://www.reza-aghaei.com/make-a-property-read-only-in-propertygrid/ (Solution #2)
public static void SetBrowsableAttribute(this PropertyDescriptor p, bool value)
{
var attributes = p.Attributes.Cast<Attribute>().Where(x => !(x is BrowsableAttribute)).ToList();
attributes.Add(new BrowsableAttribute(value));
typeof(MemberDescriptor).GetProperty("AttributeArray", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(p, attributes.ToArray());
}
}
}
WPF 中的 ObservableCollection 不会传播 NotifyPropertyChanged 事件。因此,当 TriggerTypeItems 更改时,不会触发 RefreshProperties 事件。克服此问题的一种方法是通过将 UpdatePropertyGridTriggers 中的 NotifyOfPropertyChange(nameof(propertyName));
更改为 NotifyPropertyChange("")
来强制发生 NotifyPropertyChanged 事件
在 WPF 中,我希望有这样一个视图模型,在 PropertyGrid
中它应该显示一个 CheckComboBox
使我能够动态地 show/hide 其他属性(基于选择)。
我用以下 属性 的内容填充 CheckComboBox
:
// A collection used as the data source for the CheckComboBox.
[RefreshProperties(RefreshProperties.Repaint)]
public ObservableCollection<TriggerTypeItem> TriggerTypes { get; } = new ObservableCollection<TriggerTypeItem>();
其中 TriggerTypeItem
实现了 INotifyPropertyChanged
接口。
但是,似乎对 TriggerTypeItem
元素的更改不会导致 TriggerTypes
属性被视为已修改,因此 - Browsable
属性的动态更改未反映在属性 网格。
(SetBrowsableAttribute()
函数工作正常,您可以通过切换 ShouldShow
属性 的复选框看到。)
我应该怎么做才能达到预期的效果?
MainWindow.xaml
<Window x:Class="WpfPlayground.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="300">
<Grid>
<xctk:PropertyGrid x:Name="PropertyGridControl">
<xctk:PropertyGrid.EditorDefinitions>
<xctk:EditorTemplateDefinition TargetProperties="TriggerTypes">
<xctk:EditorTemplateDefinition.EditingTemplate>
<DataTemplate>
<xctk:CheckComboBox ItemsSource="{Binding Instance.TriggerTypes}"
DisplayMemberPath="TriggerType"
SelectedMemberPath="Selected" />
</DataTemplate>
</xctk:EditorTemplateDefinition.EditingTemplate>
</xctk:EditorTemplateDefinition>
</xctk:PropertyGrid.EditorDefinitions>
</xctk:PropertyGrid>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfPlayground
{
public partial class MainWindow : Window
{
private readonly EventViewModel eventViewModel = new EventViewModel();
public MainWindow()
{
InitializeComponent();
PropertyGridControl.SelectedObject = eventViewModel;
}
}
public abstract class PropertyChangedBase : Component, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyOfPropertyChange([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public enum TriggerType
{
Timer,
}
public class EventViewModel : PropertyChangedBase
{
private bool _shouldShow;
private readonly Dictionary<TriggerType, string> _triggerViewModels = new Dictionary<TriggerType, string>()
{
{ TriggerType.Timer, nameof(ToogleProperty) }
};
// A "regular" checkbox for toogling.
[RefreshProperties(RefreshProperties.Repaint)]
public bool ShouldShow
{
get { return _shouldShow; }
set
{
TypeDescriptor.GetProperties(this)[nameof(ToogleProperty)].SetBrowsableAttribute(value);
_shouldShow = value;
NotifyOfPropertyChange(nameof(ShouldShow));
}
}
// A collection used as the data source for the CheckComboBox.
[RefreshProperties(RefreshProperties.Repaint)]
public ObservableCollection<TriggerTypeItem> TriggerTypes { get; } = new ObservableCollection<TriggerTypeItem>();
// The property to be toogled.
[ReadOnly(true)]
public string ToogleProperty { get; set; } = "(Toggle me!)";
public EventViewModel()
{
ShouldShow = true;
foreach (TriggerType val in Enum.GetValues(typeof(TriggerType)))
{
TriggerTypes.Add(new TriggerTypeItem(triggerType: val, eventModel: this) { Selected = false });
}
}
/// <summary>
/// Dynamically sets the <c>Browsable</c> attribute.
/// </summary>
public void UpdatePropertyGridTriggers(TriggerType triggerType, bool newBrowsableState)
{
if (_triggerViewModels.TryGetValue(triggerType, out string propertyName))
{
TypeDescriptor.GetProperties(this)[propertyName].SetBrowsableAttribute(newBrowsableState);
NotifyOfPropertyChange(nameof(propertyName));
}
}
}
/// <summary>
/// An item in the CheckCoboBox.
/// </summary>
public class TriggerTypeItem : PropertyChangedBase
{
private TriggerType _triggerType;
private bool _selected;
public EventViewModel EventModel { get; private set; }
public TriggerType TriggerType
{
get { return _triggerType; }
set
{
_triggerType = value;
NotifyOfPropertyChange(nameof(TriggerType));
}
}
public bool Selected
{
get { return _selected; }
set
{
if (_selected != value)
{
_selected = value;
EventModel?.UpdatePropertyGridTriggers(TriggerType, value);
NotifyOfPropertyChange(nameof(Selected));
}
}
}
public TriggerTypeItem(TriggerType triggerType, EventViewModel eventModel)
{
EventModel = eventModel;
TriggerType = triggerType;
}
}
public static class PropertyDescriptorExtensions
{
/// See: http://www.reza-aghaei.com/make-a-property-read-only-in-propertygrid/ (Solution #2)
public static void SetBrowsableAttribute(this PropertyDescriptor p, bool value)
{
var attributes = p.Attributes.Cast<Attribute>().Where(x => !(x is BrowsableAttribute)).ToList();
attributes.Add(new BrowsableAttribute(value));
typeof(MemberDescriptor).GetProperty("AttributeArray", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(p, attributes.ToArray());
}
}
}
WPF 中的 ObservableCollection 不会传播 NotifyPropertyChanged 事件。因此,当 TriggerTypeItems 更改时,不会触发 RefreshProperties 事件。克服此问题的一种方法是通过将 UpdatePropertyGridTriggers 中的 NotifyOfPropertyChange(nameof(propertyName));
更改为 NotifyPropertyChange("")