Changing column based on header ComboBox selection

2019-08-23 11:46发布

So currently I have a DataGrid that looks like this: Current DataGridView

The goal behind this is to create an import utility. The data received in CSV format isn't always structured the same way. The columns change order, sometimes they only provide partial data.

I'd like for the user to be able to select where each column is directed. I'm running into a few problems and I was hoping someone more experienced could direct me.

First, the type of the data entered is still restricted. So for example, if column 1 is an integer, then it won't allow for text input. I was planning on writing an event handler for when the headers ComboBox changed to change the BindingExpression. but realistically this just needs to be a typeless column. Which would be entered into the actual table based on the comboBox selection afterwards.

I'm also unsure how to identify/get at the ComboBox from the ViewModel when it's generated this way.

xaml

<DataGrid x:Name="ImportTable" 
          ItemsSource="{Binding displayTable}"
          AutoGeneratingColumn="OnAutoGeneratingColumn"
          AutoGenerateColumns="True"
          CanUserAddRows="True" 
          CanUserDeleteRows="True"
          EnableColumnVirtualization="True"
          EnableRowVirtualization="True"
          MaxWidth="1300"
          MaxHeight="600"
          />

xaml.cs

//i keeps track of the column index, temporary solution to preset columns
private int i;
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{   

    var cb = new ComboBox();
    cb.ItemsSource = (DataContext as EnterValueDialogViewModel).displayTable.Columns;
    cb.DisplayMemberPath = "ColumnName";
    cb.SelectedValue = e.PropertyName.ToString();
    cb.SelectedIndex = i;
    e.Column.Header = cb;
    i++;
}

1条回答
倾城 Initia
2楼-- · 2019-08-23 12:08

Here's a rough sketch of the basic wiring. We've got a class called ColumnHeaderViewmodel that represents the header for one column. The MainViewModel has a collection of those as well as a DataTable property called Data. That DataTable is one relatively tolerable way to display arbitrary, known-only-at-runtime CSV in a DataGrid. Another way is an ObservableCollection<ExpandoObject>, with some extra code to generate the columns. We can go into that if need be.

It's a bad idea to create WPF UI in C#. Anybody who's experienced with WPF avoids that kind of thing if he possibly can.

We'll store the CSV in a separate structure, perhaps List<List<String>> or something, and every time the column headers change, we'll repopulate the DataTable from that collection. That's your problem, I'm not addressing it. See the "TODO" comments.

This code is a complete self-contained example that lets you rename columns from header comboboxes. You can add other stuff (e.g. datatype, or an include/exclude flag) to ColumnHeaderViewModel and to the header template.

First, we'll illustrate how to put comboboxes in DataGrid column headers. Don't do it in C#.

<DataGrid
    ItemsSource="{Binding Data}"
    AutoGeneratingColumn="DataGrid_AutoGeneratingColumn"
    >
    <DataGrid.ColumnHeaderStyle>
        <Style TargetType="DataGridColumnHeader">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <ComboBox
                            ItemsSource="{Binding ColumnNames}"
                            SelectedItem="{Binding ColumnName}"
                            />
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGrid.ColumnHeaderStyle>
</DataGrid>

Second, in the codebehind file for the above XAML (maybe your MainWindow, maybe a UserControl), we'll wire up the ColumnHeaderViewModels; they're in ViewModel.Columns:

    public MainViewModel ViewModel => DataContext as MainViewModel;

    private void DataGrid_AutoGeneratingColumn(object sender, 
            DataGridAutoGeneratingColumnEventArgs e)
    {
        e.Column.Header = ViewModel.Columns
                            .FirstOrDefault(c => c.ColumnName == e.PropertyName);
    }

MainViewModel:

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        //  TODO:
        //      Load CSV

        //  TODO: 
        //      These are fake. Get actual column names from CSV
        ColumnNames = new List<string> { "Foo", "Bar", "Numeric" };

        Columns = new List<ColumnHeaderViewModel>();

        foreach (var name in ColumnNames)
        {
            Data.Columns.Add(new DataColumn(name));
            var col = new ColumnHeaderViewModel(ColumnNames, name);
            col.NameChanging += Column_NameChanging;
            Columns.Add(col);
        }

        UpdateDataTableFromCSVRows();
    }

    private void Column_NameChanging(object sender, ValueChangingEventArgs<String> e)
    {
        var col = sender as ColumnHeaderViewModel;

        //  Swap names. DataTable throws an exception on column name collisions
        var otherCol = Columns.FirstOrDefault(c => c != col && c.ColumnName == e.NewValue);

        if (e.OldValue != null && otherCol != null)
        {
            //  Use Rename() method so it won't raise NameChanged again.
            //  However, UpdateDataTableFromCSVRows() should be clever enough 
            //  to do nothing in cases where the column order is unchanged.
            otherCol.Rename(e.OldValue);
        }

        UpdateDataTableFromCSVRows();
    }

    protected void UpdateDataTableFromCSVRows()
    {
        //  TODO:
        //      Update the DataTable from the CSV rows, based on the new 
        //      column names. 
    }

    public List<ColumnHeaderViewModel> Columns { get; private set; }

    public List<String> ColumnNames { get; private set; }

    private DataTable _data = default(DataTable);
    public DataTable Data
    {
        get { return _data; }
        private set
        {
            if (value != _data)
            {
                _data = value;
                OnPropertyChanged();
            }
        }
    }
}

ColumnHeaderViewModel

public class ValueChangingEventArgs<T> : EventArgs
{
    public ValueChangingEventArgs(T oldValue, T newValue)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }

    public T OldValue { get; private set; }
    public T NewValue { get; private set; }
}

public class ColumnHeaderViewModel : ViewModelBase
{
    public ColumnHeaderViewModel(List<String> names, string name)
    {
        ColumnNames = names;
        ColumnName = name;
    }

    public List<String> ColumnNames { get; private set; }

    public event EventHandler<ValueChangingEventArgs<String>> NameChanging;


    #region ColumnName Property
    private String _columnName = default(String);
    public String ColumnName
    {
        get { return _columnName; }
        set
        {
            if (value != _columnName)
            {
                var oldName = ColumnName;
                _columnName = value;
                OnPropertyChanged();
                NameChanging?.Invoke(this, new ValueChangingEventArgs<string>(oldName, ColumnName));
            }
        }
    }
    #endregion ColumnName Property

    //  Rename without raising NameChanging
    public void Rename(string newName)
    {
        _columnName = newName;
        OnPropertyChanged(nameof(ColumnName));
    }
}
查看更多
登录 后发表回答