Search code examples
c#wpfdatagrid

How to create a Binding for dynamically created columns in DataGrid?


I'm working on WPF app development. And one of our dialog has a DataGrid. This DataGrid should display an information about some 3D points. For this point we have a class PointViewModel with 4 required main properties - Name, X, Y, Z:

public class PointViewModel : ViewModelBase
{
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            NotifyPropertyChanged(nameof(Name));
        }
    }

    public double X
    {
        get => _x;
        set
        {
            _x = value;
            NotifyPropertyChanged(nameof(X));
        }
    }

    public double Y
    {
        get => _y;
        set
        {
            _y = value;
            NotifyPropertyChanged(nameof(Y));
        }
    }

    public double Z
    {
        get => _z;
        set
        {
            _z = value;
            NotifyPropertyChanged(nameof(Z));
        }
    }

    public ObservableCollection<PointCustomPropViewModel> CustomProperties => _customProperties;

    public PointViewModel(string name, double x, double y, double z)
    {
        // in the real code this collection is empty at the beginning, but for the test I added 2 custom properties where I just "know" their names and values.
        _customProperties = new ObservableCollection<PointCustomPropViewModel>()
        {
            new PointCustomPropViewModel("Test Prop 1", "Hello"),
            new PointCustomPropViewModel("Test Prop 2", "World"),
        };

        Name = name;
        X = x;
        Y = y;
        Z = z;
    }

    private readonly ObservableCollection<PointCustomPropViewModel> _customProperties;
    private string _name;
    private double _x;
    private double _y;
    private double _z;


}

Also each Point can have some custom properties. We don't know any information about these properties when app just started, so they should be created dynamically. For these custom properties each Point has a collection of CustomProperties. Each PointCustomPropViewModel has information about property name and property value, that's all:

public class PointCustomPropViewModel : ViewModelBase
{
    public string PropName
    {
        get => _propName;
        set
        {
            _propName = value;
            NotifyPropertyChanged(nameof(PropName));
        }
    }

    public string PropValue
    {
        get => _propValue;
        set
        {
            _propValue = value;
            NotifyPropertyChanged(nameof(PropValue));
        }
    }

    public PointCustomPropViewModel(string propName, string propValue)
    {
        PropName = propName;
        PropValue = propValue;
    }

    private string _propName;
    private string _propValue;
}

Here I created a test view model for main dialog that just creates a collection of points with some random parameters:

public class MainWindowViewModel : ViewModelBase
{
    public ObservableCollection<PointViewModel> Points => _points;

    public ICollectionView PointsView => _pointsView;

    public MainWindowViewModel()
    {
        _points = new ObservableCollection<PointViewModel>();
        _pointsView = CollectionViewSource.GetDefaultView(_points);

        Initialize();
    }

    private void Initialize()
    {
        var _random = new Random();

        for (int _i = 0; _i < 3; _i++)
        {
            double _randomX = _random.NextDouble();
            double _randomY = _random.NextDouble();
            double _randomZ = _random.NextDouble();

            _points.Add(new PointViewModel($"Point {_i}", _randomX, _randomY, _randomZ));
        }

        NotifyPropertyChanged(nameof(PointsView));
    }

    private readonly ObservableCollection<PointViewModel> _points;
    private readonly ICollectionView _pointsView;
}

So here I create a 3 test points, each point also has 2 custom properties. Custom properties can be added or removed any time when user do something on dialog. And any time at the beginning we don't know actual count, names and values for these custom properties.

But our DataGrid should display information about all points including information about custom properties with possibility to edit value of each main or custom properties. So, for example, I have some test XAML code where I created a DataGrid, added binding to ItemSource with PointsView property and binding between main 4 columns and main properties in PointViewModel object:

<DataGrid AutoGenerateColumns="False"
          ItemsSource="{Binding PointsView}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Name"
                            Width="150"
                            Binding="{Binding Name}"/>
        <DataGridTextColumn Header="X"
                            Width="75"
                            Binding="{Binding X}"/>
        <DataGridTextColumn Header="Y"
                            Width="75"
                            Binding="{Binding Y}"/>
        <DataGridTextColumn Header="Z"
                            Width="75"
                            Binding="{Binding Z}"/>
    </DataGrid.Columns>
</DataGrid>

It give me next dialog:

But how can I generate a new columns based on collection with binding to Value property in CustomProperties collection for each point? For example, my points have new custom property "Test Prop 1", this property has value "Hello". So I should generate a new column with header "Test Prop 1" and each row in this column should display value for related point (in this case value will be "Hello"). And when, for example, I try to edit value in this column in the first row and change it to "Hi" - it should find a first object in collection Points, find a PointCustomPropViewModel object with Name property = "Test Prop 1" in its collection CustomProperties and update Value property from "Hello" to "Hi".


Solution

  • You need to generate the columns dynamically somehow, for example based on the number of PointCustomPropViewModel items in the first PointViewModel in the source collection:

    var pointViewModel = viewModel.Points[0];
    foreach (var i = 0; i < pointViewModel.CustomProperties.Count; i++)
    {
        var customProperty = pointViewModel.CustomProperties[i];
        dataGrid.Columns.Add(new DataGridTextColumn()
        {
            Header = customProperty.PropName,
            Binding = new Binding($"CustomProperties[{i}].PropValue")
        });
    }