带有复选框的 C# WPF 目录树视图:检查构建项目失败,PropertyChanged 为空
Posted
技术标签:
【中文标题】带有复选框的 C# WPF 目录树视图:检查构建项目失败,PropertyChanged 为空【英文标题】:C# WPF Directory Treeview with checkboxes: check items on building fails with empty PropertyChanged 【发布时间】:2021-10-03 13:36:09 【问题描述】:在 WPF 窗口中,我显示了一个带有复选框的树视图,其中包含 Pc 上的磁盘/目录。当用户展开一个节点时,一个事件调用 folder_Expanded 添加该节点的子目录。
应该发生的是某些目录显示颜色(这有效),如果在 XML 文件中找到某些目录,则会检查它们。然后,用户可以选中或取消选中(子)目录,之后修改的目录选择再次存储在该 xml 文件中。
但是,我无法使用某个目录检查该 treeviewitem 中的复选框。在展开事件的代码中,我用一个示例目录对其进行了测试。背景颜色工作正常,但 IsSelected 行什么也没做。原因是 PropertyChanged 为 null,因此它不会创建 PropertyChangedEventArgs 的实例。我想说我拥有一切:从 INotifyPropertyChanged 继承并在 XAML 中分配为 DataContext 的模型,并通过此模型设置 XAML 中定义的 CheckBox 的属性 IsChecked。 我错过了什么?
或者我想知道我是否可以直接将复选框设置为选中,无需数据绑定,就像我设置背景颜色一样?数据绑定的问题是当它不起作用时,无法调试代码,它就是不起作用....
一开始:
SelectFilesModel selectFilesModel = new SelectFilesModel();
public SelectFiles()
InitializeComponent();
Window_Loaded();
void folder_Expanded(object sender, RoutedEventArgs e)
TreeViewItem item = (TreeViewItem)sender;
if (item.Items.Count == 1 && item.Items[0] == dummyNode)
item.Items.Clear();
try
foreach (string s in Directory.GetDirectories(item.Tag.ToString()))
TreeViewItem subitem = new TreeViewItem();
subitem.Header = s.Substring(s.LastIndexOf("\\") + 1);
subitem.Tag = s;
subitem.FontWeight = FontWeights.Normal;
subitem.Items.Add(dummyNode);
subitem.Expanded += new RoutedEventHandler(folder_Expanded);
if (s.ToLower() == "c:\\temp") // Sample directory to test
subitem.Background = Brushes.Yellow; // This works!
selectFilesModel.IsChecked = true; // Eventually PropertyChanged is always null!!
item.Items.Add(subitem);
catch (Exception e2)
MessageBox.Show(e2.Message + " " + e2.InnerException);
XAML 如下所示:
<Window.DataContext>
<local:SelectFilesModel/>
</Window.DataContext>
<Grid>
<TreeView x:Name="foldersItem" SelectedItemChanged="foldersItem_SelectedItemChanged" Width="Auto" Background="#FFFFFFFF" BorderBrush="#FFFFFFFF" Foreground="#FFFFFFFF">
<TreeView.Resources>
<Style TargetType="x:Type TreeViewItem">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="Binding
RelativeSource=RelativeSource
Mode=FindAncestor,
AncestorType=x:Type TreeViewItem,
Path=Header,
Converter=x:Static local:HeaderToImageConverter.Instance"
/>
<TextBlock Name="DirName" Text="Binding" Margin="5,0" />
<CheckBox Name="cb" Focusable="False" IsThreeState="True" IsChecked="Binding IsChecked ,UpdateSourceTrigger=PropertyChanged" VerticalAlignment="Center"/> </StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
模型如下:
public class SelectFilesModel : INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
bool? _isChecked = false;
public bool? IsChecked
get return _isChecked;
set this.SetIsChecked(value, true, true);
void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
if (value == _isChecked)
return;
_isChecked = value;
RaisePropertyChanged("IsChecked");
void RaisePropertyChanged(string prop)
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop));
// SelectFilesModel
【问题讨论】:
您有两个 SelectFilesModel 实例,一个在 XAML 中声明为窗口的 DataContext,另一个在后面的代码中创建。删除 XAML 声明并在 SelectFiles 构造函数中设置DataContext = selectFilesModel;
。
感谢 Clemens,但我也已经尝试过了(稍后添加了 XAML 代码)。仍然 PropertyChanged 仍然为空。还有其他建议吗?
【参考方案1】:
看看你如何初始化 TreeView 会很有趣。看起来selectFilesModel
确实不是任何数据绑定的来源。它甚至不是你树的一部分。
您正在手动添加TreeViewItem
(这不是一个好主意 - 请查看您的问题,如果您专注于处理数据模型,则不会存在该问题)。因为直接添加了TreeViewItem
元素,所以TreeViewItem
的DataContext
就是item本身。HeaderTemplate
的 DataContext
是标头值,在您的情况下是 string
。你看selectFilesModel
从来没有涉及。CheckBox.IsChecked
当前绑定到这个字符串,我们都知道string
没有属性IsChecked
。
你应该做的是使用SelectFilesModel
创建树。
以下示例是您修改后的代码。它没有经过测试和没有编辑器的编写,因此可能包含一些小错误。显示模式应该足够了。
另请注意,Directory.EnumerateDirectories
在您的场景中的性能将比Directory.GetDirectories
好得多。
创建一个枚举来表达不同的状态。每个状态都将映射到您使用触发器在 XAML 中设置的颜色。
enum DirectoryState
Default = 0,
Special
然后修改SelectFilesModel
以允许引用其子目录(子目录)并添加State
枚举属性
public class SelectFilesModel : INotifyPropertyChanged
// TODO::Implement constructor to initialize properties
public event PropertyChangedEventHandler PropertyChanged;
bool? _isChecked = false;
public bool? IsChecked
get return _isChecked;
set this.SetValue(value, ref _isChecked, true, true);
DirectoryState _state;
public DirectoryState State
get return _state;
set this.SetValue(value, ref _state, true, true);
string _path;
public string Path
get return _path;
set this.SetValue(value, ref _path, true, true);
public ObservableCollection<SelectFilesModel> Subdirectories get;
void SetValue<TValue>(TValue value, ref TValue field, bool updateChildren, bool updateParent, [CallerMemberName] string propertyName = null)
if (value == field)
return;
field = value;
RaisePropertyChanged(propertName);
void RaisePropertyChanged(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
然后使用模型构建树。请注意,由于Expanded
是一个路由事件,因此您不必显式订阅每个项目。只听路由事件。
ObservableCollection<SelectFilesModel> TreeRoot get;
public SelectFiles()
InitializeComponent();
Window_Loaded();
foldersItem.AddHandler(TreeViewItem.ExpandedEvent, new RoutedEventHandler(folder_Expanded)));
TreeRoot = new ObservableCollection<SelectFilesModel>() new SelectFilesModel() ;
foldersItem.ItemsSource = TreeRoot;
void folder_Expanded(object sender, RoutedEventArgs e)
var item = (sender as TreeViewItem).DataContext as SelectFilesModel;
if (item.Subdirectories.Count == 1 && item.Subdirectories[0] == dummyNode)
item.Subdirectories.Clear();
try
foreach (string s in Directory.EnumerateDirectories(item.Path))
var subitem = new SelectFilesModel() Path = Path.GetDirectoryName(s) ;
subitem.Subdirectories.Add(dummyNode);
if (subitem.Path.ToLower() == "c:\\temp") // Sample directory to test
subitem.State = DirectoryState.Special; // This works!
subitem.IsChecked = true; // This should work too
item.Subdirectories.Add(subitem);
catch (Exception e2)
MessageBox.Show(e2.Message + " " + e2.InnerException);
最后使用适当的触发器定义数据模板并将其添加到例如TreeView.Resources
:
<HierarchicalDataTemplate DataType="x:Type SelectFilesModel
ItemsSource="Binding Subdirectories">
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="Binding
RelativeSource=RelativeSource
Mode=FindAncestor,
AncestorType=x:Type TreeViewItem,
Path=Header,
Converter=x:Static local:HeaderToImageConverter.Instance"
/>
<TextBlock Name="DirName" Text="Binding Path" Margin="5,0" />
<CheckBox Name="cb" Focusable="False" IsThreeState="True" IsChecked="Binding IsChecked ,UpdateSourceTrigger=PropertyChanged" VerticalAlignment="Center"/> </StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="Binding State" Value="x:Static DirectoryState.Special">
<Setter TargetName="DirName" Property="Foreground" Value="Yellow" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
【讨论】:
您好 BionicCode,感谢您所做的所有工作,并且:您在没有编辑器的情况下生成了所有这些代码,这给我留下了深刻的印象。我需要一些时间来检查和实施这一点,所以我稍后会回来发表评论。我手动添加 TreeView 的原因是,我认为否则您在 OC 中读取 PC 的整个目录结构,这比仅添加扩展节点的目录需要更多时间,但我看到您仍然这样做。我会尽快通知你的! 是的,您仍然可以逐级读取目录。这是数据逻辑,应该始终与 UI 逻辑分离。但我强烈建议使用接受EnumerationsOptions
的Directory.EnumerateDirectories
重载,当EnumerationsOptions.IgnoreInaccessible
设置为true
时,在枚举期间尝试访问禁止目录时避免UnauthorizedAccessException
。但请注意,这种重载仅在 .NET 5 中可用(我认为也是 .NET Core 和 Standard)。
在清除(仅)一些代码问题后,例如在 HierarchicalDataTemplate 处缺少引号,我可以启动程序但出现异常“在使用 ItemsSource 之前项目集合必须为空”。搜索得知这是一个很难找到的 XAML 问题。我终于放弃并重新启动这个屏幕作为 Winforms 屏幕。检查复选框(和文本颜色)+阅读它们有效。并且不需要 IMO 经常导致无法调试的问题的数据绑定,因此我尽可能避免使用它。不过感谢您的努力!以上是关于带有复选框的 C# WPF 目录树视图:检查构建项目失败,PropertyChanged 为空的主要内容,如果未能解决你的问题,请参考以下文章