0

I'm building an app which will display a list of buttons in 2D grid, similar to How to populate a WPF grid based on a 2-dimensional array

The challenge is to tell which button (in terms of i, j) was clicked in the buttonClick event handler.

My initial thought was to make the button name include a string representation of the i,j (e.g. 'button_2_3'), but it is not possible to specify the name using '{binding ...}'.

It is NOT possible to use the button label (button.content), as the application requirement are to display non-unique labels on the buttons (the buttons label represent the piece on the cell, similar to chess, where the 'Q' label indicate the queens position).

The best alternative seems to store a representation of the row/column in the CommandParameter attribute of the button.

My Question: How to populate the CommandParameter of each button in such a way that it will be simple to extract the (i, j) of the clicked button ? Is there a better alternative to attach user defined data to a button ?

From: How to populate a WPF grid based on a 2-dimensional array

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

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

And

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;
}

private void buttonClick(object sender, EventArgs e)
{
  //do something or...
  Button clicked = (Button) sender;
  MessageBox.Show("Button's name is: " + clicked.Name);
}

alt text

dash-o
  • 13,723
  • 1
  • 10
  • 37
  • _Is there a better alternative to attach user defined data to a button ?_ Use the Tag. – Nawed Nabi Zada Jan 21 '20 at 10:56
  • UniformGrid would be a better choice for ItemsPanel (with 1-dimensional ItemsSource). see examples: https://stackoverflow.com/q/43097851/1506454, https://stackoverflow.com/q/37145391/1506454 – ASh Jan 21 '20 at 12:18
  • Ended up merging the two proposed solutions - using the 'Tag' to store an object with 'Row' and 'Column' and 'Value' attributes. – dash-o Jan 21 '20 at 14:27

2 Answers2

2

You may bind the Button's Tag property

<Button Tag="{Binding}" .../>

and use it in a Clicked handler like this:

private void Button_Click(object sender, RoutedEventArgs e)
{
    int value = (int)((Button)sender).Tag;
    int row = value / 10;
    int column = value % 10;

    Debug.WriteLine("Clicked row {0}, column {1}", row, column);
}

It may also be simpler to use a single ItemsControl with a UniformGrid as ItemsPanel:

<ItemsControl ItemsSource="{Binding}"
              HorizontalAlignment="Left" VerticalAlignment="Top">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Columns="5"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding}" Tag="{Binding}"
                    Height="40" Width="50" Margin="4" Click="Button_Click"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

and bind its ItemsSource to a List<int>:

public MainWindow()
{
    InitializeComponent();

    var list = new List<int>();

    for (int i = 0; i < 5; i++)
    {
        list.AddRange(Enumerable.Range(i * 10, 5));
    }

    DataContext = list;
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thanks for the input about the `uniformGrid`. I'm not able to use 'content' as a hint to the button - as I indicated in the question - the labels (`content` of the button) shows the piece in the cell (Similar to chess, where 'Q' will indicate the queen location). I've highlighted this requirement in the post. – dash-o Jan 21 '20 at 10:31
  • Please see the edited answer. You may also bind the Tag property. – Clemens Jan 21 '20 at 11:02
1

You can use a type for a button viewmodel instead of the list of int.

Then bind 'name' inside of the content and button will contains your object in the DataContext.

class ButtonViewModel
{
  public string Name {get;set;} 
  public int Value {get;set;} 
}  

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

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

        for (int j = 0; j < 5; j++)
        {
            var model = new ButtonViewModel();
            model.Value = i * 10 + j;
            model.Name = "Q: "+ model.Value;
            lsts[i].Add(model);
        }
    }

    InitializeComponent();

    lst.ItemsSource = lsts;
}

private void buttonClick(object sender, EventArgs e)
{
  //do something or...
  Button clicked = (Button) sender;
  MessageBox.Show("Button's name is: " + (clicked.DataContext as ButtonViewModel).Name);
}

WPF:

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

P.s. Instead of the button "сlick" event "command" property can be used. Than command property should be added to the ButtonViewModel. This is a proper MVVM way. Also ButtonViewModel should implement IPropertyChanged interface to notify interface about the value changes.

Ievgen
  • 4,261
  • 7
  • 75
  • 124
  • For the WPF, does the DataTemplate 'DataTemplate_Level2' need to be updated in any way ? It's not listed in the answer, and I'm not sure if it implies no change needed. – dash-o Jan 21 '20 at 14:41
  • @dash-o yes, you should bind a 'Content' to a 'Name' property. – Ievgen Jan 21 '20 at 14:45