带有复选框的 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元素,所以TreeViewItemDataContext就是item本身。HeaderTemplateDataContext 是标头值,在您的情况下是 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 逻辑分离。但我强烈建议使用接受EnumerationsOptionsDirectory.EnumerateDirectories 重载,当EnumerationsOptions.IgnoreInaccessible 设置为true 时,在枚举期间尝试访问禁止目录时避免UnauthorizedAccessException。但请注意,这种重载仅在 .NET 5 中可用(我认为也是 .NET Core 和 Standard)。 在清除(仅)一些代码问题后,例如在 HierarchicalDataTemplate 处缺少引号,我可以启动程序但出现异常“在使用 ItemsSource 之前项目集合必须为空”。搜索得知这是一个很难找到的 XAML 问题。我终于放弃并重新启动这个屏幕作为 Winforms 屏幕。检查复选框(和文本颜色)+阅读它们有效。并且不需要 IMO 经常导致无法调试的问题的数据绑定,因此我尽可能避免使用它。不过感谢您的努力!

以上是关于带有复选框的 C# WPF 目录树视图:检查构建项目失败,PropertyChanged 为空的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中,如何在加载树视图后将所有树节点设置为 true [重复]

如何在 Datagrid WPF c# 中添加复选框

使用复选框创建 WPF Windows 资源管理器树视图

如何在 wpf 的分层数据模板中显示树视图项的上下文菜单

创建一个带有标签和复选框的树,检测其父项是否已选中

通过列表视图检查动态生成的复选框时出现问题