Bind custom header controls in DataGrid
Asked Answered
E

3

8

I have a custom column header where each column's header has TextBox which contains name of the column and ComboBox, which contains information about the type of the column, e.g. "Date", "Number", etc.

I'm trying to bind ComboBox and keep its value somewhere, so that when user selects new value from ComboBox I can recreate table with the column's type changed. Basically all I need is to store somehow each ComboBox's value in a list somehow. I want to do the same with TextBox which should contain name of the column.

This is what I have so far.

<DataGrid x:Name="SampleGrid" Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2" ItemsSource="{Binding SampledData}">
            <DataGrid.Resources>
                <Style TargetType="{x:Type DataGridColumnHeader}">
                    <Setter Property="ContentTemplate">
                        <Setter.Value>
                            <DataTemplate>
                                <StackPanel>
                                    <TextBox Text="{Binding ., Mode=OneWay}"/>
                                    <ComboBox>
                                        // How can I set it from ViewModel?
                                        <ComboBoxItem Content="Date"></ComboBoxItem>
                                        <ComboBoxItem Content="Number"></ComboBoxItem>
                                    </ComboBox>
                                </StackPanel>
                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </DataGrid.Resources>
        </DataGrid>

ViewModel:

private DataTable _sampledData = new DataTable();

public DataTable SampledData
{
    get => _sampledData;
    set { _sampledData = value; NotifyOfPropertyChange(() => SampledData); }
}

Solutions in code behind are welcome too as long as I can pass the mappings to ViewModel later.

EDIT: I've been trying to make this work with a List of ViewModels, but no luck:

public class ShellViewModel : Screen
{

    public List<MyRowViewModel> Rows { get; set; }

    public ShellViewModel()
    {
        Rows = new List<MyRowViewModel>
        {
            new MyRowViewModel { Column1 = "Test 1", Column2= 1 },
            new MyRowViewModel { Column1 = "Test 2", Column2= 2 }
        };
    }
}

View

<DataGrid ItemsSource="{Binding Rows}">
    <DataGrid.Resources>
        <Style TargetType="{x:Type DataGridColumnHeader}">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <StackPanel>
                            <TextBox Text="{Binding ., Mode=OneWay}"/>
                            <ComboBox ItemsSource="{Binding ??????}" />
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGrid.Resources>
</DataGrid>

Row

public class MyRowViewModel : PropertyChangedBase
{
    public string Column1 { get; set; }
    public int Column2 { get; set; }
}

EDIT2:

To clarify, I need a solution that will handle dynamic number of columns, so some files may store 3 columns and some might store 40 columns. I use this for parsing csv files to later display the data. In order to do that I have to know what types of values the file contains. Because some types may be ambiguous, I let the user decide which types they want. This is identical to Excel's "Load From File" wizard.

The wizard loads a small chunk of data (100 records) and allows user to decide what type the columns are. It automatically parses the columns to:

  1. Let user see how the data will look like
  2. Validate if the column can actually be parsed (e.g. 68.35 cannot be parsed as DateTime)

Another thing is naming each column. Someone might load csv with each column named C1, C2... but they want to assign meaningful names such as Temperature, Average. This of course has to be parsed later too, because two columns cannot have the same name, but I can take care of this once I have a bindable DataGrid.

Elva answered 27/2, 2018 at 18:9 Comment(10)
Convert your datatable to a viewmodel - you can create an IEnumerable<ViewModelType> (IList/List if you want) which contains each row as a viewmodel instance - bind this to your datagrid instead and bind the combo box column - you can then reconstitute this viewmodel back into the datatable.Lactary
@Lactary I'm not sure I understand. How would I bind column's header to this? And why do I need rows to be in separate viewmodels? I need to bind only headers not rows.Elva
You never need to cave in to a solution in the code behind :) This is possible the way that charleh was going about. I would personally use an IEnumerable<ModelType> and add the ColumnType object to the model and bind to the ColumnTypeChrysler
@Lactary I'm sorry, but I have been playing around with this for hours and the furthest I got was to display each column as a property, which is not what I want, because I don't know how many columns I will have, so I cannot hardcode them. I updated my question with what I tried.Elva
Your columns can be a list, you can bind to a list too so it can workLactary
@Lactary I tried that and it creates 1 column with each row containing string Collection, so I assume it cannot convert List object .Elva
@Elva Can you share some graphical view of the output you want? I could not get you question fully.Genu
MyRowViewModel has different types for each column such as string, int? But you want to be able to change the types of the properties/columns on your row type? Your question does not make a great deal of sense. Ignoring that you should be able to use a bit of a binding hack to allow your column header to bind to properties on ShellViewModel, does that sound like it might help?Removable
@AlexHopeO'Connor I want to add a combobox for each column header. This combobox will have values such as Number, Date. If users selects Date then every value in this column should be parsed to DateTime and updated. Because I cannot modify type of column I have to recreate the whole table with this column's type changed, but that's a detail. I also want to be able to change name of each column's header, so that's why I have TextBox in the template. I don't know how many columns I will have. I will update the question with more detail.Elva
I am actually starting work on an abstract tool for importing CSV files of varying format/column counts so I have been thinking about how to do this myself recently, except in my case the user is not allowed to specify the data type as the target data types are already known.Removable
F
3

Let's break your problem into parts and solve each part separately.

First, the DataGrid itemsource, to make things easier, let's say that our DataGrid has only two columns, column 1 and column 2. A basic model for the DataGrid Items should looks like this:

public class DataGridModel
{
    public string FirstProperty { get; set; }   
    public string SecondProperty { get; set; }   
}

Now, assuming that you have a MainWindow (with a ViewModel or the DataContext set to code behind) with a DataGrid in it , let's define DataGridCollection as its ItemSource:

private ObservableCollection<DataGridModel> _dataGridCollection=new ObservableCollection<DataGridModel>()
    {
        new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
        new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
        new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"}
    };
    public ObservableCollection<DataGridModel> DataGridCollection
    {
        get { return _dataGridCollection; }
        set
        {
            if (Equals(value, _dataGridCollection)) return;
            _dataGridCollection = value;
            OnPropertyChanged();
        }
    }

Second, now the interesting part, the columns structure. Let's define a model for your DataGrid's columns, the model will hold all the required properties to set your DataGrid columns, including:

-DataTypesCollection: a collection that holds the combobox itemsource. -HeaderPropertyCollection: a collection of Tuples, each Tuple represent a column name and a data type, the data type is basically the selected item of column's combobox.

 public class DataGridColumnsModel:INotifyPropertyChanged
    {
        private ObservableCollection<string> _dataTypesCollection = new ObservableCollection<string>()
        {
            "Date","String","Number"
        };
        public ObservableCollection<string> DataTypesCollection         
        {
            get { return _dataTypesCollection; }
            set
            {
                if (Equals(value, _dataTypesCollection)) return;
                _dataTypesCollection = value;
                OnPropertyChanged();
            }
        }

        private ObservableCollection<Tuple<string, string>> _headerPropertiesCollection=new ObservableCollection<Tuple<string, string>>()
        {
            new Tuple<string, string>("Column 1", "Date"),
            new Tuple<string, string>("Column 2", "String")

        };   //The Dictionary has a PropertyName (Item1), and a PropertyDataType (Item2)
        public ObservableCollection<Tuple<string,string>> HeaderPropertyCollection
        {
            get { return _headerPropertiesCollection; }
            set
            {
                if (Equals(value, _headerPropertiesCollection)) return;
                _headerPropertiesCollection = value;
                OnPropertyChanged();
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Now in you MainWindow's viewmodel (or codebehind) define an instance of the DataGridColumnsModel that we will be using to hold our DataGrid structure:

        private DataGridColumnsModel _dataGridColumnsModel=new DataGridColumnsModel();
    public DataGridColumnsModel DataGridColumnsModel
    {
        get { return _dataGridColumnsModel; }
        set
        {
            if (Equals(value, _dataGridColumnsModel)) return;
            _dataGridColumnsModel = value;
            OnPropertyChanged();
        }
    }

Third, getting the column's TextBox's value. For that w'll be using a MultiBinding and a MultiValueConverter, the first property that w'll be passing to the MultiBinding is the collection of tuples that we define (columns' names and datatypes): HeaderPropertyCollection, the second one is the current column index that w'll retrieve from DisplayIndex using an ancestor binding to the DataGridColumnHeader:

<TextBox >
    <TextBox.Text>
       <MultiBinding Converter="{StaticResource GetPropertConverter}">
            <Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="DataGridColumnsModel.HeaderPropertyCollection"/>
            <Binding  Path="DisplayIndex" Mode="OneWay" RelativeSource="{RelativeSource RelativeSource={x:Type DataGridColumnHeader}}"/>
      </MultiBinding> 
  </TextBox.Text>

The converter will simply retrieve the right item using the index from collection of tuples:

public class GetPropertConverter:IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            var theCollection = values[0] as ObservableCollection<Tuple<string, string>>;
            return (theCollection?[(int)values[1]])?.Item1; //Item1 is the column name, Item2 is the column's ocmbobox's selectedItem
        }
        catch (Exception)
        {
            //use a better implementation!
            return null;
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Fourth, The last part is to update the DataGrid's ItemSource when the Combobox's selection changed, for that you could use the Interaction tools defined in System.Windows.Interactivity namespace (which is part of Expression.Blend.Sdk, use NuGet to install it: Install-Package Expression.Blend.Sdk):

<ComboBox ItemsSource="{Binding DataGridColumnsModel.DataTypesCollection,RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
   <i:Interaction.Triggers>
      <i:EventTrigger EventName="SelectionChanged">
          <i:InvokeCommandAction Command="{Binding UpdateItemSourceCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
      </i:EventTrigger>
  </i:Interaction.Triggers>
</ComboBox>

Each time the selectionChanged event occurred, update your DataGrid's ItemSource in the UpdateItemSourceCommand that should be added to your mainWindow's ViewModel:

 private RelayCommand _updateItemSourceCommand;
    public RelayCommand UpdateItemSourceCommand
    {
        get
        {
            return _updateItemSourceCommand
                   ?? (_updateItemSourceCommand = new RelayCommand(
                       () =>
                       {
                           //Update your DataGridCollection, you could also pass a parameter and use it.
                           //Update your DataGridCollection based on DataGridColumnsModel.HeaderPropertyCollection
                       }));
        }
    }

Ps: the RelayCommand class i am using is part of GalaSoft.MvvmLight.Command namespace, you could add it via NuGet, or define your own command.

Finally here the full xaml code:

Window x:Class="WpfApp1.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:WpfApp1"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
    <local:GetPropertConverter x:Key="GetPropertConverter"/>
</Window.Resources>
<Grid>
    <DataGrid x:Name="SampleGrid" ItemsSource="{Binding DataGridCollection}" AutoGenerateColumns="False">
        <DataGrid.Resources>
            <Style TargetType="{x:Type DataGridColumnHeader}">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <StackPanel>
                                <TextBox >
                                    <TextBox.Text>
                                        <MultiBinding Converter="{StaticResource GetPropertConverter}">
                                            <Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="DataGridColumnsModel.HeaderPropertyCollection"/>
                                            <Binding  Path="DisplayIndex" Mode="OneWay" RelativeSource="{RelativeSource AncestorType={x:Type DataGridColumnHeader}}"/>
                                        </MultiBinding> 
                                    </TextBox.Text>
                                </TextBox>
                                <ComboBox ItemsSource="{Binding DataGridColumnsModel.DataTypesCollection,RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
                                    <i:Interaction.Triggers>
                                        <i:EventTrigger EventName="SelectionChanged">
                                           <i:InvokeCommandAction Command="{Binding UpdateItemSourceCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
                                        </i:EventTrigger>
                                    </i:Interaction.Triggers>
                                </ComboBox>
                            </StackPanel>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </DataGrid.Resources>
        <DataGrid.Columns>
            <DataGridTextColumn Header="First Column" Binding="{Binding FirstProperty}" />
            <DataGridTextColumn Header="Second Column" Binding="{Binding SecondProperty}"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

And view models / codebehind:

public class GetPropertConverter:IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            var theCollection = values[0] as ObservableCollection<Tuple<string, string>>;
            return (theCollection?[(int)values[1]])?.Item1; //Item1 is the column name, Item2 is the column's ocmbobox's selectedItem
        }
        catch (Exception)
        {
            //use a better implementation!
            return null;
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
public class DataGridColumnsModel:INotifyPropertyChanged
{
    private ObservableCollection<string> _dataTypesCollection = new ObservableCollection<string>()
    {
        "Date","String","Number"
    };
    public ObservableCollection<string> DataTypesCollection         
    {
        get { return _dataTypesCollection; }
        set
        {
            if (Equals(value, _dataTypesCollection)) return;
            _dataTypesCollection = value;
            OnPropertyChanged();
        }
    }

    private ObservableCollection<Tuple<string, string>> _headerPropertiesCollection=new ObservableCollection<Tuple<string, string>>()
    {
        new Tuple<string, string>("Column 1", "Date"),
        new Tuple<string, string>("Column 2", "String")

    };   //The Dictionary has a PropertyName (Item1), and a PropertyDataType (Item2)
    public ObservableCollection<Tuple<string,string>> HeaderPropertyCollection
    {
        get { return _headerPropertiesCollection; }
        set
        {
            if (Equals(value, _headerPropertiesCollection)) return;
            _headerPropertiesCollection = value;
            OnPropertyChanged();
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class DataGridModel
{
    public string FirstProperty { get; set; }   
    public string SecondProperty { get; set; }   
}
public partial class MainWindow : Window,INotifyPropertyChanged
{
    private RelayCommand _updateItemSourceCommand;
    public RelayCommand UpdateItemSourceCommand
    {
        get
        {
            return _updateItemSourceCommand
                   ?? (_updateItemSourceCommand = new RelayCommand(
                       () =>
                       {
                           //Update your DataGridCollection, you could also pass a parameter and use it.
                           MessageBox.Show("Update has ocured");
                       }));
        }
    }

    private ObservableCollection<DataGridModel> _dataGridCollection=new ObservableCollection<DataGridModel>()
    {
        new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
        new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
        new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"}
    };
    public ObservableCollection<DataGridModel> DataGridCollection
    {
        get { return _dataGridCollection; }
        set
        {
            if (Equals(value, _dataGridCollection)) return;
            _dataGridCollection = value;
            OnPropertyChanged();
        }
    }

    private DataGridColumnsModel _dataGridColumnsModel=new DataGridColumnsModel();
    public DataGridColumnsModel DataGridColumnsModel
    {
        get { return _dataGridColumnsModel; }
        set
        {
            if (Equals(value, _dataGridColumnsModel)) return;
            _dataGridColumnsModel = value;
            OnPropertyChanged();
        }
    }

    public MainWindow()
    {
        InitializeComponent();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Result:

enter image description here

Update

You will achieve the same result by setting AutoGenerateColumns="True" and creating you columns dynamically.

Fecit answered 5/3, 2018 at 17:7 Comment(7)
Thank you, but the same as the previous answer, this doesn't handle dynamic number of columns. I need this to display csv files for later manipulation. Each csv contains different number of columns, so I cannot hardcode them.Elva
@Elva I don't see why this won't work with an autogeneratedcolumns=True !Fecit
Ok, I will try to make this work with any number of columns and get back to you with results.Elva
Indeed, setting AutoGenerateColumns="True" and generating the columns dynamically at runtime works like a charm with the above code.Fecit
Sorry for late response, long week... It looks like you are right, auto generating columns dynamically at runtime is possible, but it requires constructing dynamic collection for ObservableCollection<dynamic> _dataGridCollection. This is ok, because I can use ExpandoObject to construct this type. Kind of nasty, but will do for now. Do you think this is a good idea or am I missing something obvious here?Elva
It looks messy indeed, i don't see an obvious work around that, may be rebuilding a custom control that ensure that extensibility would be an option, but an expensive one. In you place i would stick with this solution at least as a starter.Fecit
Thank you very much. This steered me in the right direction.Elva
B
2

Try this.

Window1.xaml

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:WpfApplication1"
        Title="Window1" Height="300" Width="300">

    <Window.Resources>
        <this:RowDataConverter x:Key="RowDataConverter1" />
    </Window.Resources>
    <Grid>

        <DataGrid ItemsSource="{Binding Rows, Mode=OneWay}">
            <DataGrid.Columns>
                <DataGridTextColumn>
                    <DataGridTextColumn.Binding>
                        <MultiBinding Converter="{StaticResource RowDataConverter1}">
                            <Binding Path="Column1" Mode="OneWay" />
                            <Binding Path="Column1OptionString" Mode="OneWay" RelativeSource="{RelativeSource AncestorType=Window, Mode=FindAncestor}" />
                        </MultiBinding>
                    </DataGridTextColumn.Binding>

                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <TextBlock Text="Column Header 1" />
                                <ComboBox ItemsSource="{Binding ColumnOptions, Mode=OneWay, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                                          SelectedValue="{Binding Column1OptionString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
                                          SelectedValuePath="Option">
                                    <ComboBox.ItemTemplate>
                                        <DataTemplate DataType="this:ColumnOption">
                                            <TextBlock Text="{Binding Option, Mode=OneTime}" />
                                        </DataTemplate>
                                    </ComboBox.ItemTemplate>
                                </ComboBox>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Window1.xaml.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication1
{
    public partial class Window1 : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public List<RowData> Rows { get; set; }
        public List<ColumnOption> ColumnOptions { get; set; }

        private string _column1OptionString;
        public string Column1OptionString
        {
            get
            {
                return _column1OptionString;
            }
            set
            {
                _column1OptionString = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Column1OptionString"));
            }
        }

        public Window1()
        {
            InitializeComponent();

            ColumnOptions = new List<ColumnOption>() {
                new ColumnOption(){ Option = "String", StringFormat = "" },
                new ColumnOption(){ Option = "Int32", StringFormat = "" }
            };

            Rows = new List<RowData>() {
                new RowData(){ Column1 = "01234" }
            };

            _column1OptionString = "String";

            this.DataContext = this;
        }
    }

    public class ColumnOption
    {
        public string Option { get; set; }
        public string StringFormat { get; set; }
    }

    public class RowData : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private object _column1;
        public object Column1
        {
            get
            {
                return _column1;
            }
            set
            {
                _column1 = value;
                if (PropertyChanged!= null)
                PropertyChanged(this, new PropertyChangedEventArgs("Column1"));
            }
        }
    }

    public class RowDataConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (values[1] == null)
                return values[0].ToString();

            switch (values[1].ToString())
            {
                case "String":
                    return values[0].ToString();
                case "Int32":
                    Int32 valueInt;
                    Int32.TryParse(values[0].ToString(), out valueInt);
                    return valueInt.ToString();
                default:
                    return values[0].ToString();
            }
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

UPDATE
based on @FCin comment
"This is nice, but I use this to load csv files and the number of columns changes depending of csv file. Here I have to hardcore each column, but some files may have 1 column and some might have 30 columns".

Assume your csv file using format:
line1: Headers,
line2: Data Type,
line3-end: Records.

Example data1.csv:
ColumnHeader1,ColumnHeader2
Int32,String
1,"A"
2,"B"
3,"C"

I try to parse csv file using TextFieldParser, then generate the DataGrid's columns programmatically.

Window2.xaml

<Window x:Class="WpfApplication1.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window2" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0">
            <Label Content="File:" />

            <ComboBox x:Name="FileOption"
                      SelectionChanged="FileOption_SelectionChanged">
                <ComboBox.Items>
                    <Run Text="Data1.csv" />
                    <Run Text="Data2.csv" />
                </ComboBox.Items>

            </ComboBox>

        </StackPanel>

        <DataGrid x:Name="DataGrid1" Grid.Row="1"
                  AutoGenerateColumns="False"
                  ItemsSource="{Binding ListOfRecords, Mode=OneWay}">
        </DataGrid>

    </Grid>

</Window>

Window2.xaml.cs

using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;

namespace WpfApplication1
{
    public partial class Window2 : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        List<myDynamicObject> _listOfRecords;
        public List<myDynamicObject> ListOfRecords
        {
            get
            {
                return _listOfRecords;
            }
        }

        public Window2()
        {
            InitializeComponent();
            DataContext = this;
        }

        public void LoadData(string fileName)
        {
            _listOfRecords = new List<myDynamicObject>();
            myDynamicObject record;

            TextFieldParser textFieldParser = new TextFieldParser(fileName);
            textFieldParser.TextFieldType = FieldType.Delimited;
            textFieldParser.SetDelimiters(",");

            string[] headers = null;
            string[] dataTypes = null;
            string[] fields;

            int i = 0;
            while(!textFieldParser.EndOfData)
            {
                fields = textFieldParser.ReadFields();

                if (i == 0)
                {
                    headers = fields;
                }
                else if (i == 1)
                {
                    dataTypes = fields;
                }
                else
                {
                    record = new myDynamicObject();
                    for (int j = 0; j < fields.Length; j++)
                    {
                        switch(dataTypes[j].ToLower())
                        {
                            case "string":
                                record.SetMember(headers[j], fields[j]);
                                break;
                            case "int32":
                                Int32 data;
                                if (Int32.TryParse(fields[j], out data))
                                {
                                    record.SetMember(headers[j], data);
                                }
                                break;
                            default:
                                record.SetMember(headers[j], fields[j]);
                                break;
                        }
                    }

                    _listOfRecords.Add(record);
                }

                i += 1;
            }

            PropertyChanged(this, new PropertyChangedEventArgs("ListOfRecords"));

            DataGrid1.Columns.Clear();
            for (int j = 0; j < headers.Length; j++)
            {
                DataGrid1.Columns.Add(new DataGridTextColumn()
                {
                    Header = headers[j],
                    Binding = new Binding()
                    {
                        Path = new PropertyPath(headers[j]),
                        Mode = BindingMode.OneWay
                    }
                });
            }
        }

        private void FileOption_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            LoadData((FileOption.SelectedItem as Run).Text);
        }
    }

    public class myDynamicObject : DynamicObject
    {
        Dictionary<string, object> dictionary = new Dictionary<string, object>();

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            string name = binder.Name;
            return dictionary.TryGetValue(name, out result);
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            dictionary[binder.Name] = value;
            return true;
        }

        public void SetMember(string propertyName, object value)
        {
            dictionary[propertyName] = value;
        }
    }
}
Beauvais answered 5/3, 2018 at 10:2 Comment(1)
This is nice, but I use this to load csv files and the number of columns changes depending of csv file. Here I have to hardcore each column, but some files may have 1 column and some might have 30 columns.Elva
A
2

This is not exactly a complete answer but more a hint towards what I think your looking to do, if so you can query me for additional information.

I think what you want to do is define let say a DataGridColumDef type such as this:

 public class DataGridColumnDef : NotifyPropertyChangeModel
    {
        public string Name
        {
            get => _Name;
            set => SetValue(ref _Name, value);
        }
        private string _Name;

        public Type DataType
        {
            get => _DataType;
            set => SetValue(ref _DataType, value);
        }
        private Type _DataType;

        public DataGridColumnDef(string name, Type type)
        {
            Name = name ?? throw new ArgumentNullException(nameof(name));
            DataType = type ?? throw new ArgumentNullException(nameof(type));
        }
    }

Then I imagine your view model acting as the data context for the DataGrid could look something like this:

public class MainViewModel : NotifyPropertyChangeModel
    {
        public ObservableList<DataGridColumnDef> ColumnDefinitions
        {
            get => _ColumnDefinitions;
            set => SetValue(ref _ColumnDefinitions, value);
        }
        private ObservableList<DataGridColumnDef> _ColumnDefinitions;

        public ObservableList<DataGridRowDef> RowDefinitions
        {
            get => _RowDefinitions;
            set => SetValue(ref _RowDefinitions, value);
        }
        private ObservableList<DataGridRowDef> _RowDefinitions;

        public MainViewModel()
        {
            // Define initial columns
            ColumnDefinitions = new ObservableList<DataGridColumnDef>()
            {
                new DataGridColumnDef("Column 1", typeof(string)),
                new DataGridColumnDef("Column 2", typeof(int)),
            };

            // Create row models from initial column definitions
            RowDefinitions = new ObservableList<DataGridRowDef>();
            for(int i = 0; i < 100; ++i)
            {
                RowDefinitions.Add(new DataGridRowDef(ColumnDefinitions));
                // OR
                //RowDefinitions.Add(new DataGridRowDef(ColumnDefinitions, new object[] { "default", 10 }));
            }
        }
    }

This way on the main view model you could subscribe to collection/property changed events in the ColumnDefinitions property and then re-create the rows collection.

Now the trick that I am not 100% sure would work, but not sure why it wouldn't, is making your DataGridRowDef type inherit from DynamicObject so you can spoof members and their values, something like this:

public class DataGridRowDef : DynamicObject
    {
        private readonly object[] _columnData;
        private readonly IList<DataGridColumnDef> _columns;

        public static object GetDefault(Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }
            return null;
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return _columns.Select(c => c.Name).Union(base.GetDynamicMemberNames());
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            var columnNames = _columns.Select(c => c.Name).ToList();
            if(columnNames.Contains(binder.Name))
            {
                var columnIndex = columnNames.IndexOf(binder.Name);
                result = _columnData[columnIndex];
                return true;
            }
            return base.TryGetMember(binder, out result);
        }

        public DataGridRowDef(IEnumerable<DataGridColumnDef> columns, object[] columnData = null)
        {
            _columns = columns.ToList() ?? throw new ArgumentNullException(nameof(columns));
            if (columnData == null)
            {
                _columnData = new object[_columns.Count()];
                for (int i = 0; i < _columns.Count(); ++i)
                {
                    _columnData[i] = GetDefault(_columns[i].DataType);
                }
            }
            else
            {
                _columnData = columnData;
            }
        }
    }

Anyway if this kind of solution seems approachable to you I can try and work through it a bit more possibly.

Aminta answered 6/3, 2018 at 0:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.