WPF - Binding events to class methods of Item in ItemControl

3.4k views Asked by At

I'm a bit new to WPF/XAML (though I've learnt C#) and would really appreciate any help for my question. I did look around other posts and google for a while but I can't seem to find a satisfactory or detailed answer to get me going on with my project. Please look below for details. Thanks you in advance!

Objective

I have a class called Tile that consists of a few properties and an event handler. I also have an ItemControl that has a button (as by the DataTemplate), and whose ItemSource is a collection of Tiles.

Now, I want to bind the "Click" event of the Button so as to invoke the Event Handler method defined in the class Tile.

In other words when I click the button of any item in the ItemControl, the method handler of the corresponding Tile instance (from the collection) must be invoked. How would I tackle this problem?

Below is the entire code, simplified to avoid distraction:

XAML

<Window x:Class="SampleWPF.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="300" Width="300">

    <!-- Make a ItemControl for "Tile"s. -->
    <ItemsControl x:Name="TileList">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <!-- Wire the click event of this Button 
                 to event handler in the Tile class. -->
                <Button Content="Show"></Button>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

</Window>

CODE-BEHIND

namespace SampleWPF
{
    public partial class MainWindow : Window
    {
        ObservableCollection<Tile> tiles;

        public MainWindow()
        {
            InitializeComponent();

            // Adding some sample data for testing.
            tiles = new ObservableCollection<Tile>();
            tiles.Add(new Tile("Item 1"));
            tiles.Add(new Tile("Item 2"));

            TileList.ItemsSource = tiles;
        }
    }

    public class Tile : INotifyPropertyChanged
    {
        public string Data
        { /* Accessors and PropertyNotifiers */ }

        public Tile(string data)
        { /* Initializing and assigning "Data" */ }

        // INotifyPropertyChanged implementation...
        // { ... }

        // This event handler should be bound to the Button's "Click" event
        // in the DataTemplate of the Item.
        public void ShowButton_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Viewing item from: " + this.Data);
        }
    }
}

Hence, if I click the first "Show" button, the output should be "Viewing item from: Item 1" and if I click the second "Show" Button, the output should be "Viewing item from: Item 2".

So what is the recommended/efficient way to do this? Is my code inappropriate for this requirement?

3

There are 3 answers

6
MajkeloDev On BEST ANSWER

Let me start with some basics: Don't assign itemsource in codeBehind - use Binding like this:

<Controll ItemSource="{Binding MyObservableCollection}"/>

There are many ways You can achieve this. I think that using this.Data is not the best solution for this. For example if Your tail have ID or something You can assign this id to button CommandParameter like below

<Button CommanParameter="{Binding Path=ID}" Click="ShowButton_Click"/>

And then in Your button_click event u can 'catch' this like this:

public void ShowButton_Click(object sender, EventArgs e)
{
  int ID = int.Parse(((Button)sender).CommandParameter.ToString());
}

EDIT

To use this binding You need to set DataContext. You can do this in ctor like this:

 public MainWindow()
    {
        InitializeComponent();

        // Adding some sample data for testing.
        tiles = new ObservableCollection<Tile>();
        tiles.Add(new Tile("Item 1"));
        tiles.Add(new Tile("Item 2"));
        // below You are setting a datacontext of a MainWindow to itself
        this.DataContext = this;
    }

ANOTHER EDIT

Let's assume Your tail class have property called ID. If You bound this ID to Button.CommandParameter You can later retrieve the tile with linq like this:

public void ShowButton_click(object sender, EventArgs e)
{
   int MyId = int.Parse(((Button)sender).CommandParameter.ToString());
   Tile TileIWasSearchingFor = (from t in tiles where t.ID == MyId select t).First();
// do something with tile You found
}
5
toadflakz On

Event handlers are the wrong approach - use Commands and more importantly MVVM.

As I can see that you are new (and probably from a WinForms or ASP.NET background) you should read this blog to understand how your thinking needs to change - this is the most important part to understand before tackling WPF: http://rachel53461.wordpress.com/2012/10/12/switching-from-winforms-to-wpfmvvm/

You should also read Kent Boogart's blog on how MVVM works from base principles: http://kentb.blogspot.co.uk/2009/03/view-models-pocos-versus.html

2
crazyGamer On

Well since my requirement was rather "simple", I've managed a work around, avoiding commands. Thanks to the answer here by MajkeloDev: https://stackoverflow.com/a/27419974/3998255 for guidance.

This is the final event handler:

public void ShowButton_Click(object sender, EventArgs e)
    {
        Tile requestingTile = (sender as Button).DataContext as Tile;
        if(requestingTile != null)
            MessageBox.Show("Viewing item from: " + this.Data); 
            // Or whatever else you want to do with the object...
    }

Also, adding the ItemSource as a XAML attribute:

<ItemsControl x:Name="TileList" ItemsSource="{Binding tiles}">

And setting DataContext in constructor of MainWindow:

public MainWindow()
{ 
    this.DataContext = this;
    // Whatever else you want to do...
}

Well it works as required.