0
votes

I have a WPF DataGrid that I bind to a DataTable. I don't like doing this but the data is coming from a delimited text file, and I don't know how many fields (columns) that the table will contain going in. Programatically this seems to be the easiest way to accomplish this (using MVVM and avoiding code behind), but given that I want two way binding, perhaps this won't work.

The DataGrid is defined like this in the view:

        <DataGrid x:Name="dataGrid" ItemsSource="{Binding FileTable, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
              HorizontalAlignment="Stretch" Margin="0,60,0,0" VerticalAlignment="Stretch">
    </DataGrid>

The ViewModel sets up the datatable by reading a text file, and adds two boolean values to the end of each row. I want the boolean values to map to the checkboxes in the DataGrid, but they don't, and I don't get any events in the viewmodel when the values are changed. I am thinking that I need to change out the datatable as seen in other related questions, but they are all responding to the viewmodel changing the view (like a button that adds a column), rather than having the change come from the datagrid within the view.

For context here is the FileTable member in my ViewModel:

private DataTable _fileTable;
public DataTable FileTable
{
    get
    {
        return _fileTable;
    }
    set
    {
        if (value != _fileTable)
        {
            _fileTable = value;
            NotifyPropertyChanged("FileTable");
        }
    }
}

And here is the code that creates the datatable from a text file:

public DataTable ParseFileToTable(Document doc, string PlaceHolders)
{
    if (dt == null)
    {
        dt = new DataTable();
    }
    else dt.Clear();

    if (filepath == null) 
    {
        OpenFileDialog dlg = new OpenFileDialog();
        dlg.DefaultExt = ".txt"; // Default file extension
        dlg.Filter = "Text documents (.txt)|*.txt"; // Filter files by extension

        Nullable<bool> result = dlg.ShowDialog();
        if (result != true) return null;

        filepath = dlg.FileName;
        StreamReader r = new StreamReader(filepath);
        string line = r.ReadLine(); // First Line is Column Names
        string[] h_line = line.Split('\t'); // tab delimeter is hardcoded for now
        for(int i = 0; i < h_line.Count(); i++) 
        {
            dt.Columns.Add(h_line[i]);
        }
        dt.Columns.Add(new DataColumn("Exists", typeof(bool)));
        dt.Columns.Add(new DataColumn("Placeholder", typeof(bool)));


        //read the rest of the file
        while (!r.EndOfStream)
        {
            line = r.ReadLine();
            string [] a_line = line.Split('\t');
            DataRow nRow = dt.NewRow();
            for(int i = 0; i < h_line.Count(); i++)
            {
                nRow[h_line[i]] = a_line[i];
            }
            nRow["Exists"] = DoesSheetExist(doc, h_line[0], a_line[0]);
            nRow["Placeholder"] = IsAPlaceholder(a_line[0], PlaceHolders);
            dt.Rows.Add(nRow);
        }
    }
    return dt;
}
2
You seem to have a bunch of XAML missing - you haven't shown your columns.slugster
I don't define the columns because I don't know how many there are .... This is why I am binding to a data table. This is the extent of the xaml. It works except that the two way binding does not get back to the datatable.Paul Gibson

2 Answers

0
votes

You need to create DatagridColumns dynamically using Behavior

    /// <summary>
    /// Creating dymanic columns to the datagrid
    /// </summary>
    public class ColumnsBindingBehaviour : Behavior<DataGrid>
    {
        public ObservableCollection<DataGridColumn> Columns
        {
            get { return (ObservableCollection<DataGridColumn>)base.GetValue(ColumnsProperty); }
            set { base.SetValue(ColumnsProperty, value); }
        }
        public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns",
            typeof(ObservableCollection<DataGridColumn>), typeof(ColumnsBindingBehaviour),
                new PropertyMetadata(OnDataGridColumnsPropertyChanged));
        private static void OnDataGridColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            var context = source as ColumnsBindingBehaviour;
            var oldItems = e.OldValue as ObservableCollection<DataGridColumn>;
            if (oldItems != null)
            {
                foreach (var one in oldItems)
                    context._datagridColumns.Remove(one);
                oldItems.CollectionChanged -= context.collectionChanged;
            }
            var newItems = e.NewValue as ObservableCollection<DataGridColumn>;
            if (newItems != null)
            {
                foreach (var one in newItems)
                    context._datagridColumns.Add(one);
                newItems.CollectionChanged += context.collectionChanged;
            }
        }
        private ObservableCollection<DataGridColumn> _datagridColumns = new ObservableCollection<DataGridColumn>();
        protected override void OnAttached()
        {
            base.OnAttached();
            this._datagridColumns = AssociatedObject.Columns;
        }
        private void collectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    if (e.NewItems != null)
                        foreach (DataGridColumn one in e.NewItems)
                            _datagridColumns.Add(one);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    if (e.OldItems != null)
                        foreach (DataGridColumn one in e.OldItems)
                            _datagridColumns.Remove(one);
                    break;
                case NotifyCollectionChangedAction.Move:
                    _datagridColumns.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;
                case NotifyCollectionChangedAction.Reset:
                    _datagridColumns.Clear();
                    if (e.NewItems != null)
                        foreach (DataGridColumn one in e.NewItems)
                            _datagridColumns.Add(one);
                    break;
            }
        }
    }

ViewModel Property as follows

        //Datagrid Column collection in Viewmodel
        private ObservableCollection<DataGridColumn> dataGridColumns;
        public ObservableCollection<DataGridColumn> DataGridColumns
        {
            get
            {
                return dataGridColumns;
            }
            set
            {
                dataGridColumns = value;
                OnPropertyChanged();
            }
        }

And Create datatable,Bind to it as follows,

    //Getting column names from datatable
    string[] columnNames = (from dc in dt.Columns.Cast<DataColumn>() select dc.ColumnName).ToArray();

    //Add two of your columns
    dt.Columns.Add(new DataColumn("Exists", typeof(bool)));
    dt.Columns.Add(new DataColumn("Placeholder", typeof(bool)));


    //Create DataGrid Column and bind datatable fields
    foreach (string item in columnNames)
    {
                        if (item.Equals("your Normal Column"))
                        {
                            DataGridColumns.Add(new DataGridTextColumn() { Header = "Normal Column", Binding = new Binding("Normal Column Name"), Visibility = Visibility.Visible});
                        }

                        else if (!item.Contains("your Bool column"))
                        {
                            //Creating checkbox control 

                          FrameworkElementFactory checkBox = new FrameworkElementFactory(typeof(CheckBox));
                         checkBox.SetValue(CheckBox.HorizontalAlignmentProperty, HorizontalAlignment.Center);

                            checkBox.Name = "Dynamic name of your check box";
                            //Creating binding
                            Binding PermissionID = new Binding(item); 

                            PermissionID.Mode = BindingMode.TwoWay;

                            PermissionID.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;


                            checkBox.SetBinding(CheckBox.TagProperty, BindingVal);

                            DataTemplate d = new DataTemplate();
                            d.VisualTree = checkBox;
                            DataGridTemplateColumn dgTemplate = new DataGridTemplateColumn();
                            dgTemplate.Header = item;
                            dgTemplate.CellTemplate = d;
                            DataGridColumns.Add(dgTemplate);
                        }

                    }

Finally Namespace in Xaml

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:vm="clr-namespace:YourProject.ViewModels"
<DataGrid  AutoGenerateColumns="False"
                      ItemsSource="{Binding Datatable,
 UpdateSourceTrigger=PropertyChanged, Mode=TwoWay,IsAsync=True}">
                <i:Interaction.Behaviors>
                    <vm:ColumnsBindingBehaviour Columns="{Binding DataContext.DataGridColumns, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
                </i:Interaction.Behaviors>
            </DataGrid>
0
votes

While I try to avoid code behind, I think my use in this solution is acceptable, as the view is simply preparing to display the ViewModel which has a dynamic size (or shape).

Instead of reading to a DataTable, I read to an object that contains a list of strings and two booleans. In my ViewModel I have an observable collection of this object. The datagrid is initialized in the code behind like this (just the list of strings shown, the two checkbox columns don't require the loop):

    public MainWindow(FileParametersViewModel vm)
    {
        InitializeComponent();
        DataContext = vm;
        dataGrid.ItemsSource = vm.lParams;
        for (int i = 0; i < vm.ParamNames.Count(); i++)
        {
            DataGridTextColumn col = new DataGridTextColumn();
            col.Header = vm.ParamNames[i];
            string path = String.Format("pArray[{0}]", i);
            col.Binding = new Binding(path);
            dataGrid.Columns.Add(col);
        }
    }

And my collection object:

public class FileSheetParameters
{
    public FileSheetParameters()
    {
        SheetExists = false;
        IsPlaceholder = false;
        pArray = new List<string>();
    }

    public bool SheetExists { get; set; }
    public bool IsPlaceholder { get; set; }
    public List<string> pArray { get; set; }
}

This seems to me to be the simplest method . . . I have not fully tested it but it appears to work so far. Will update if I find something else not working . . .