0
votes

I have a DataGrid with 2 static columns (Name and Age). I want to be able to dynamically add a variable number of additional columns to my DataGrid because I do not know beforehand how many additional columns there will be. I would also like for the Resulting DataGrid to be editable.

I have created a 'MyData' Class that I would like to use for my DataGrid DataContext (as an ObservableCollection of MyData Objects ).

public class MyData
{
    public string NameCol { get; set; }
    public string AgeCol { get; set; }
    public List<string> CitiesList { get; set; }

}

The Dynamic columns Headers I would like to add are 'Country1', 'Country2',...,'CountryN'. The entries in each of these columns would be Cities in these Countries.

I have a method that takes as arguments a List of countryNames, and my DataGrid. For illustration sake, I am passing in:

List<string> CountryNames = new List<string>() {"France", "Kenya", "Japan"};

and I am calling:

AddNewColumnsAndData(CountryNames, this.dgr);

See Implementation of the AddNewColumnsAndData() method below:

public void AddNewColumnsAndData(List<string> countryNames, DataGrid dgr)
    {
        foreach (string countryname in countryNames)
        {
            Binding bdg = new Binding(); //Not sure what appropriate argument should go in the Binding Constructor
            bdg.NotifyOnTargetUpdated = true;
            bdg.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            bdg.Mode = BindingMode.TwoWay;//Not sure if there are other bdg Properties I should be setting

            DataGridTextColumn newCityCol = new DataGridTextColumn();
            newCityCol.Header = countryname;
            newCityCol.Binding = bdg;

            dgr.Columns.Add(newCityCol);
        };
        List<string> myCityList1 = new List<string>() {"Paris", "Nairobi", "Tokyo"};
        List<string> myCityList2 = new List<string>() { "Strasbourg", "Mombasa", "Kyoto" };
        MyData firstrow = new MyData
                          {
                              NameCol = "John",
                              AgeCol = "45",
                              CityColList = myCityList1
                          };
        MyData secondrow = new MyData
        {
            NameCol = "Mary",
            AgeCol = "34",
            CityColList = myCityList2
        };

        ObservableCollection<MyData> datacollection = new ObservableCollection<MyData>();
        datacollection.Add(firstrow);
        datacollection.Add(secondrow);

        dgr.DataContext = datacollection;

    }

The XAML for my DataGrid with the Two static columns 'Name' and 'Age':

<DataGrid x:Name="dgr" ItemsSource="{Binding}" AutoGenerateColumns="False" CanUserAddRows="False">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Name" Binding="{Binding NameCol, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
                        <DataGridTextColumn Header="Age" Binding="{Binding AgeCol, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
                    </DataGrid.Columns>
                </DataGrid>

Unfortanely, this is not quite working. I am getting this error: " exception of type 'System.InvalidOperationException': Two-way binding requires Path or XPath." That tells me that I need to change the way I am setting the binding Properties.

When I comment out the 'bdg.Mode = BindingMode.TwoWay;' line, the program runs fine, however, I am not able to edit the DataGrid, also Instead of the City names underneath the Country Headers, it is pasted MyNameSpace.MyData.

My questions are: How do I set the binding appropriately so that the cells will display the city names correctly, and also, how do I make my resulting DataGrid editable?

1

1 Answers

0
votes
int citiesListIndex = 0;
foreach (string countryname in countryNames)
{
    //  This is the Path. You're binding to CitiesList[0]
    //  CitiesList[1], etc. 
    string propertyPath = $"CitiesList[{citiesListIndex}]";

    //  Pass path to constructor. 
    Binding bdg = new Binding(propertyPath);

    //  No-op
    //bdg.NotifyOnTargetUpdated = true;

    //  Is this a read-only column? If it's read-only, this is a no-op
    bdg.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

    //  Is this a read-only column? If it's read-only, this is a no-op
    bdg.Mode = BindingMode.TwoWay;

    DataGridTextColumn newCityCol = new DataGridTextColumn();
    newCityCol.Header = countryname;
    newCityCol.Binding = bdg;

    dgr.Columns.Add(newCityCol);

    ++citiesListIndex;
}

Here's why it's what it is.

Here's a Binding in XAML

    Binding="{Binding NameCol, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>

NameCol is the Path property. In XAML, this calls the Binding(String path) constructor with one parameter:

{Binding NameCol}

This works because Binding inherits from MarkupExtension. You can write your own MarkupExtension subclasses. It turns out to be a really handy way to write valueconverters with strongly typed constant parameters, for example.

You could use the default constructor, and set Path separately:

{Binding Path=NameCol}

This is why you can't do this:

{Binding Mode=TwoWay, NameCol}

...because the unnamed "parameters" and the named "parameters" are different. Constructor parameters come first, property initializations (if any) follow.

This does the same, but then sets some properties:

{Binding NameCol, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}

It's a funny syntax.