59

I have a 2-dimensional array of objects and I basically want to databind each one to a cell in a WPF grid. Currently I have this working but I am doing most of it procedurally. I create the correct number of row and column definitions, then I loop through the cells and create the controls and set up the correct bindings for each one.

At a minimum I would like to be able to use a template to specify the controls and bindings in xaml. Ideally I would like to get rid of the procedural code and just do it all with databinding, but I'm not sure that's possible.

Here is the code I am currently using:

public void BindGrid()
{
    m_Grid.Children.Clear();
    m_Grid.ColumnDefinitions.Clear();
    m_Grid.RowDefinitions.Clear();

    for (int x = 0; x < MefGrid.Width; x++)
    {
        m_Grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star), });
    }

    for (int y = 0; y < MefGrid.Height; y++)
    {
        m_Grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star), });
    }

    for (int x = 0; x < MefGrid.Width; x++)
    {
        for (int y = 0; y < MefGrid.Height; y++)
        {
            Cell cell = (Cell)MefGrid[x, y];                    

            SolidColorBrush brush = new SolidColorBrush();

            var binding = new Binding("On");
            binding.Converter = new BoolColorConverter();
            binding.Mode = BindingMode.OneWay;

            BindingOperations.SetBinding(brush, SolidColorBrush.ColorProperty, binding);

            var rect = new Rectangle();
            rect.DataContext = cell;
            rect.Fill = brush;
            rect.SetValue(Grid.RowProperty, y);
            rect.SetValue(Grid.ColumnProperty, x);
            m_Grid.Children.Add(rect);
        }
    }

}
Daniel Plaisted
  • 16,674
  • 4
  • 44
  • 56

5 Answers5

73

The purpose of the Grid is not for real databinding, it is just a panel. I am listing down the easiest way to accomplish the visualization of a two dimensional list

<Window.Resources>
    <DataTemplate x:Key="DataTemplate_Level2">
            <Button Content="{Binding}" Height="40" Width="50" Margin="4,4,4,4"/>
    </DataTemplate>

    <DataTemplate x:Key="DataTemplate_Level1">
        <ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplate_Level2}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </DataTemplate>

</Window.Resources>
<Grid>
    <ItemsControl x:Name="lst" ItemTemplate="{DynamicResource DataTemplate_Level1}"/>
</Grid>

And in the code behind set the ItemsSource of lst with a TwoDimentional data structure.

  public Window1()
    {
        List<List<int>> lsts = new List<List<int>>();

        for (int i = 0; i < 5; i++)
        {
            lsts.Add(new List<int>());

            for (int j = 0; j < 5; j++)
            {
                lsts[i].Add(i * 10 + j);
            }
        }

        InitializeComponent();

        lst.ItemsSource = lsts;
    }

This gives you the following screen as output. You can edit the DataTemplate_Level2 to add more specific data of your object.

alt text

Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
Jobi Joy
  • 49,102
  • 20
  • 108
  • 119
  • 1
    I don't necessarily need to use a grid for the databinding, but I don't want to have to create a list of lists for the source. I want to use an object that has an indexer that takes two parameters, x and y. – Daniel Plaisted Nov 10 '08 at 02:52
  • 3
    WPF binding cant recongnize an array, it has to be an Enumerable collection. So better create a List of List and databind it. – Jobi Joy Nov 13 '08 at 04:57
  • 1
    Is it possible to use the new WPF DataGrid to achieve this? – Brian Low Jul 10 '10 at 22:03
  • This is pretty much a layout problem dataGrid will be too much for this. You can see that there is no Header needed here like DataGrid has. – Jobi Joy Jul 10 '10 at 23:17
  • But what if you did want to use the features of the DataGrid like selection, navigation, etc. How would you put a 2D array into a grid? – AKoran Oct 21 '10 at 19:39
  • 3
    Bonus points for using a reference image to display correct results. A picture is worth 2^10 words. – ford Oct 22 '11 at 00:08
  • This doesn't help if you want such features of `Grid` as sizing columns to the widest element, unfortunately. – Roman Starkov Aug 25 '12 at 11:11
  • 1
    Quick question, do you really need to use DynamicResource in this example, or could Static do as well? – Kranach May 13 '14 at 17:02
  • Is it possible to know which button was clicked (in terms of i,j) in the click event handler ? – dash-o Jan 21 '20 at 09:23
45

Here is a Control called DataGrid2D that can be populated based on a 2D or
1D array (or anything that implements the IList interface). It subclasses DataGrid and adds a property called ItemsSource2D which is used for binding against 2D or 1D sources. Library can be downloaded here and source code can be downloaded here.

To use it just add a reference to DataGrid2DLibrary.dll, add this namespace

xmlns:dg2d="clr-namespace:DataGrid2DLibrary;assembly=DataGrid2DLibrary"

and then create a DataGrid2D and bind it to your IList, 2D array or 1D array like this

<dg2d:DataGrid2D Name="dataGrid2D"
                 ItemsSource2D="{Binding Int2DList}"/>

enter image description here


OLD POST
Here is an implementation that can bind a 2D array to the WPF datagrid.

Say we have this 2D array

private int[,] m_intArray = new int[5, 5];
...
for (int i = 0; i < 5; i++)
{
    for (int j = 0; j < 5; j++)
    {
        m_intArray[i,j] = (i * 10 + j);
    }
}

And then we want to bind this 2D array to the WPF DataGrid and the changes we make shall be reflected in the array. To do this I used Eric Lippert's Ref class from this thread.

public class Ref<T>  
{ 
    private readonly Func<T> getter;  
    private readonly Action<T> setter; 
    public Ref(Func<T> getter, Action<T> setter)  
    {  
        this.getter = getter;  
        this.setter = setter;  
    } 
    public T Value { get { return getter(); } set { setter(value); } }  
} 

Then I made a static helper class with a method that could take a 2D array and return a DataView using the Ref class above.

public static DataView GetBindable2DArray<T>(T[,] array)
{
    DataTable dataTable = new DataTable();
    for (int i = 0; i < array.GetLength(1); i++)
    {
        dataTable.Columns.Add(i.ToString(), typeof(Ref<T>));
    }
    for (int i = 0; i < array.GetLength(0); i++)
    {
        DataRow dataRow = dataTable.NewRow();
        dataTable.Rows.Add(dataRow);
    }
    DataView dataView = new DataView(dataTable);
    for (int i = 0; i < array.GetLength(0); i++)
    {
        for (int j = 0; j < array.GetLength(1); j++)
        {
            int a = i;
            int b = j;
            Ref<T> refT = new Ref<T>(() => array[a, b], z => { array[a, b] = z; });
            dataView[i][j] = refT;
        }
    }
    return dataView;
}

This would almost be enough to bind to but the Path in the Binding will point to the Ref object instead of the Ref.Value which we need so we have to change this when the Columns get generated.

<DataGrid Name="c_dataGrid"
          RowHeaderWidth="0"
          ColumnHeaderHeight="0"
          AutoGenerateColumns="True"
          AutoGeneratingColumn="c_dataGrid_AutoGeneratingColumn"/>

private void c_dataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    DataGridTextColumn column = e.Column as DataGridTextColumn;
    Binding binding = column.Binding as Binding;
    binding.Path = new PropertyPath(binding.Path.Path + ".Value");
}

And after this we can use

c_dataGrid.ItemsSource = BindingHelper.GetBindable2DArray<int>(m_intArray);

And the output will look like this

alt text

Any changes made in the DataGrid will be reflected in the m_intArray.

Community
  • 1
  • 1
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • Great example, thanks for sharing it! You had a question in DataGrid2D.cs: "Better way to find this out?". I think, you can write: `bool multiDimensionalArray = type.IsArray && type.GetArrayRank() == 2;` and the if statement two lines above: `if (e.NewValue is IList && (!type.IsArray || type.GetArrayRank() <= 2))` That seems to work, couldn't find an issue during quick test of the examples. – Slauma Feb 14 '11 at 17:57
  • @Slauma: Glad you liked it and thanks for the feedback! I was hoping somebody would eventually give me an update on that :) I'll try it out and update the lib! Thanks again! – Fredrik Hedblad Feb 14 '11 at 18:45
  • I'm just testing and playing around a bit ;) There seems to be another minor glitch: I think, in the `ItemsSource2DPropertyChanged` EventHandler you need to consider the case that `e.NewValue` is `null`. When someone sets the `ItemsSource2D` to null it crashes. I just had this situation accidentally. I've simply placed a `if (e.NewValue != null)` around the whole EventHandler. It doesn't crash anymore, but I'm not sure if that's sufficient. Perhaps also `dataGrid2D.ItemsSource` must be set to `null`?? – Slauma Feb 14 '11 at 20:23
  • @Slauma: It definitely shouldn't crash when setting `ItemsSource2D` to null :) That was previously handled by the if statement but due to an optimization attempt I accidently broke it.. I uploaded a new version that implements the suggestion from your first comment and fix the `null` bug. Thanks again! – Fredrik Hedblad Feb 16 '11 at 08:09
  • Hi Meleak, would it be possible to get the source of your lib DataGrid2D? Thx Fred – Fred Feb 17 '11 at 21:52
  • Hi Fred! The link is in the post :) Maybe I did not make this very clear but the second link is a solution with a test application and the library source. http://www.mediafire.com/?9x83ca35afa8ywf – Fredrik Hedblad Feb 17 '11 at 21:52
  • Sorry, I think, I simply missed the link. Unfortunately, I cannot download it from work because of some network policies. I'll try it at home. Your library seems to be interesting (I couldn't try it yet), so I think, it could be nice to put it on Sourceforge for example. Have a nice day. Fred – Fred Feb 17 '11 at 21:52
  • Hi Meleak, thanks for the control. Nice job ! I have a question. Is it possible to start with an index of 1 instead of 0 ? Many thanks JPG –  Feb 17 '11 at 21:52
  • Yeah, nice work! It would be sweet if you could host it somewhere else, if you cannot or don't want to, just give me the OK and I'll host it. (The problem is, many businesses block the access to those file hosters). – Dänu Jun 18 '12 at 08:35
  • Nice post! You should put this on Codeplex and add it as a nuget package! Or maybe contribute to the WPF Toolkit! – Roger Far Sep 09 '12 at 19:57
  • Hi, this comment comes very late but... I can't thank you enough for this awesome control. I use it all day long, it is included in my appliactions, and it works perfectly. I am just waiting for a codeplex page :) – Damascus Sep 17 '12 at 18:35
  • This is great. Sometimes we have very simple data and this approach really simplifies things. – rollsch Sep 21 '16 at 20:56
  • @FredrikHedblad This is the best answer. Using your approach, its great! – Mikhail T. Nov 24 '16 at 18:13
5

I wrote a small library of attached properties for the DataGrid. Here is the source

Sample, where Data2D is int[,]:

<DataGrid HeadersVisibility="None"
          dataGrid2D:Source2D.ItemsSource2D="{Binding Data2D}" />

Renders: enter image description here

Johan Larsson
  • 17,112
  • 9
  • 74
  • 88
  • Surprised there is no comments to this. Nothing else I can find for doing something simple like this. Spend days trying to bind a 2D array to a gridview! So difficult when it takes less than 10 minutes in winforms – rollsch Nov 07 '16 at 05:00
1

Here is another solution based on Meleak's answer but without requiring for an AutoGeneratingColumn event handler in the code behind of each binded DataGrid:

public static DataView GetBindable2DArray<T>(T[,] array)
{
    var table = new DataTable();
    for (var i = 0; i < array.GetLength(1); i++)
    {
        table.Columns.Add(i+1, typeof(bool))
                     .ExtendedProperties.Add("idx", i); // Save original column index
    }
    for (var i = 0; i < array.GetLength(0); i++)
    {
        table.Rows.Add(table.NewRow());
    }

    var view = new DataView(table);
    for (var ri = 0; ri < array.GetLength(0); ri++)
    {
        for (var ci = 0; ci < array.GetLength(1); ci++)
        {
            view[ri][ci] = array[ri, ci];
        }
    }

    // Avoids writing an 'AutogeneratingColumn' handler
    table.ColumnChanged += (s, e) => 
    {
        var ci = (int)e.Column.ExtendedProperties["idx"]; // Retrieve original column index
        var ri = e.Row.Table.Rows.IndexOf(e.Row); // Retrieve row index

        array[ri, ci] = (T)view[ri][ci];
    };

    return view;
}
Community
  • 1
  • 1
CitizenInsane
  • 4,755
  • 1
  • 25
  • 56
-1

You may want to check out this link: http://www.thinkbottomup.com.au/site/blog/Game_of_Life_in_XAML_WPF_using_embedded_Python

If you use a List within a List you can use myList[x][y] to access a cell.

Torsten
  • 1,696
  • 2
  • 21
  • 42