简体   繁体   English

WPF TabControl 更改 SelectedIndex / SelectedItem 导致绑定错误

[英]WPF TabControl changing SelectedIndex / SelectedItem causes binding errors

I have been trying to solve this problem for about a week now, unable to find anything helpful to solve this.我已经尝试解决这个问题大约一个星期了,找不到任何有助于解决这个问题的方法。 I am rather inexperienced in the Data Binding approach WPF takes (in comparison to Unity, WinForms etc.) and was hoping someone here might be able to help out.我对 WPF 采用的数据绑定方法相当缺乏经验(与 Unity、WinForms 等相比),并希望这里有人能提供帮助。

I am working on a WPF application in C# to be able to display *.csv files in a DataGrid and compare one with the other (so I would likely need 3 TabControls: 2 of them to load in a *.csv and a final one to display results in 3 tabs (matches, differences A -> B and differences (B -> A)). I am working on a WPF application in C# to be able to display *.csv files in a DataGrid and compare one with the other (so I would likely need 3 TabControls: 2 of them to load in a *.csv and a final one在 3 个选项卡中显示结果(匹配、差异 A -> B 和差异 (B -> A))。

The problem I am struggling with is manipulating the TabControl without errors.我正在努力解决的问题是无错误地操作 TabControl。 Whenever I close a tab after I set the tab to the last tab, I get hit with a very unhelpful error message: Error;每当我在将选项卡设置为最后一个选项卡后关闭选项卡时,都会收到一条非常无用的错误消息:错误; 4; 4; null; null; TabStripPlacement; TabStripPlacement; TabItem.NoTarget; TabItem.NoTarget; Object; Object; Cannot find source: RelativeSource FindAncestor, AncestorType='System.Windows.Controls.TabControl', AncestorLevel='1'.找不到源:RelativeSource FindAncestor,AncestorType='System.Windows.Controls.TabControl',AncestorLevel='1'。

What I did notice is if I am not setting the SelectedIndex, and remove a tab, all is good (though not what I want).我注意到的是,如果我没有设置 SelectedIndex 并删除一个选项卡,一切都很好(尽管不是我想要的)。

The error can be consistantly recreated by drag and dropping a *.csv file into the tabControl, then closing it with the 'x'.通过将 *.csv 文件拖放到 tabControl 中,然后用“x”关闭它,可以一致地重新创建错误。

The XAML: XAML:

<Window x:Class="CsvComparer.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:local="clr-namespace:CsvComparer"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
    <local:TabViewModel xmlns="clr-namespace:CsvComparer"/>
</Window.DataContext>
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="20"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="20"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="20"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="20"/>
    </Grid.RowDefinitions>
    
    <TabControl Grid.Column="1" Grid.Row="1"
                x:Name="firstFileTabControl"         
                ItemsSource="{Binding Tabs}"
                SelectedIndex="{Binding SelectedIndex}"
                TabStripPlacement="Top"
                AllowDrop="True"
                Drop="FileTabControl_OnDrop"                    
                Tag="0">
        <TabControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Header}"/>
                    <Button Content="X" Click="TabCloseButton_Click" Tag="{Binding InstanceId}"/>
                </StackPanel>
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.ContentTemplate>
            <DataTemplate>
                <DataGrid ItemsSource="{Binding DataTable}" 
                          CanUserAddRows="False"
                          CanUserDeleteRows="False"
                          EnableColumnVirtualization="True"
                          CanUserResizeRows="False"/>
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
</Grid>

The Models:模型:

public sealed class TabItemModel
{
    public string Header { get; set; }
    public DataTable DataTable { get; set; }        
    public long InstanceId { get; set; }
}

public sealed class TabViewModel : INotifyPropertyChanged
{       
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<TabItemModel> Tabs { get; set; }

    private int selectedIndex = 0;

    public int SelectedIndex
    {
        get
        {
            return selectedIndex;
        }

        set
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedIndex)));
            selectedIndex = value;
        }
    }

    public TabViewModel()
    {
        Tabs = new();
    }
}

The C# Code in the MainWindow.xaml.cs: MainWindow.xaml.cs中的C#代码:

using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.Win32;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CsvComparer
{
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new TabViewModel();
    }

    #region EventHandlers

    private long instanceCount = 0;
    private void FileTabControl_OnDrop(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop))
        {
            string[] fileNames = (string[])e.Data.GetData(DataFormats.FileDrop);
            string file = fileNames.FirstOrDefault();

            TabItemModel newTab = new()
            {
                InstanceId = instanceCount++,
                Header = $"{ System.IO.Path.GetFileName(file) }",
                DataTable = GetCsvRecords(file),                    
            };

            var model = (TabViewModel)DataContext;
            model.Tabs.Add(newTab);
            
            // when commenting out this line, no errors
            model.SelectedIndex++;                                
        }
    }

    private void TabCloseButton_Click(object sender, RoutedEventArgs e)
    {
        Button button = (Button)sender;
        long id = (long)button.Tag;

        var model = (TabViewModel)DataContext;
        var item = model.Tabs.FirstOrDefault(x => x.InstanceId == id);
        if (item != null)
        {
            model.Tabs.Remove(item);
        }            
    }

    #endregion

    private CsvConfiguration CreateCsvConfig()
    {
        CsvConfiguration output = new CsvConfiguration(System.Globalization.CultureInfo.CurrentCulture)
        {
            HasHeaderRecord = true,
            // Delimiter = ", ",
            BadDataFound = new BadDataFound((data) => Debug.WriteLine($"Found faulty data: '{ data.RawRecord }'")),
            MissingFieldFound = new MissingFieldFound((data) => Debug.WriteLine($"Missing field found at Row '{ data.Index }', Header '{ data.HeaderNames }'")),
            ShouldSkipRecord = record => record.Record.All(string.IsNullOrWhiteSpace),
        };

        return output;
    }

    private DataTable GetCsvRecords(string path, CsvConfiguration config = null)
    {
        DataTable output = new();
        config ??= CreateCsvConfig();

        using (StreamReader sr = new(path))
        {
            using (CsvReader csvReader = new(sr, config))
            {
                using (CsvDataReader dataReader = new(csvReader))
                {
                    output.Load(dataReader);
                }
            }
        }

        return output;
    }
}

}    

This is my hunch as to what is happening: say you have three tabs open and you have the final tab selected (SelectedIndex=2).这是我对正在发生的事情的预感:假设您打开了三个选项卡并且您选择了最后一个选项卡 (SelectedIndex=2)。 After closing a tab, the observable collection will only contain two tab items, so you get the binding error because the value of SelectedIndex (2) is now invalid.关闭选项卡后,可观察集合将仅包含两个选项卡项,因此您会收到绑定错误,因为 SelectedIndex (2) 的值现在无效。

The fix may be as simple as updating the value of the SelectedIndex property from within TabCloseButton_Click, but do so right before you remove the tab item.修复可能就像从 TabCloseButton_Click 中更新 SelectedIndex 属性的值一样简单,但在删除选项卡项之前执行此操作。 This won't be necessary if the value of SelectedIndex is less than the index of the tab being removed (as SelectedIndex will remain valid after removal).如果 SelectedIndex 的值小于要删除的选项卡的索引,则不需要这样做(因为 SelectedIndex 在删除后仍然有效)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM