Strange behavior when using lambda expressions on WPF click buttons

My problem is hard to explain, so I created an example to show here.

When the WPF window is displayed in the example below, three buttons are displayed, each with a different text.

When I press any of these buttons, I assume that the text should be displayed in the message, but instead they all display the same message, as if they all used the event handler of the last button.

public partial class Window1 : Window { public Window1() { InitializeComponent(); var stackPanel = new StackPanel(); this.Content = stackPanel; var n = new KeyValuePair<string, Action>[] { new KeyValuePair<string, Action>("I", () => MessageBox.Show("I")), new KeyValuePair<string, Action>("II", () => MessageBox.Show("II")), new KeyValuePair<string, Action>("III", () => MessageBox.Show("III")) }; foreach (var a in n) { Button b = new Button(); b.Content = a.Key; b.Click += (x, y) => a.Value(); stackPanel.Children.Add(b); } } } 

Does anyone know what is wrong?

+2
source share
3 answers

It is because of how closures are evaluated by the compiler in a loop:

 foreach (var a in n) { Button b = new Button(); b.Content = a.Key; b.Click += (x, y) => a.Value(); stackPanel.Children.Add(b); } 

The compiler assumes that in closing you will need the context of a , since you are using a.Value , so it creates one lambda expression that uses the value of a . However, a has scope in the whole loop, so it will just have the last value assigned to it.

To get around this, you need to copy a to a variable inside the loop, and then use it:

 foreach (var a in n) { Button b = new Button(); b.Content = a.Key; // Assign a to another reference. var a2 = a; // Set click handler with new reference. b.Click += (x, y) => a2.Value(); stackPanel.Children.Add(b); } 
+4
source

Unintuitive, huh? The reason is that lambda expressions capture variables from outside the expression.

When the for loop last runs, the variable a set to the last element in the array n , and after that it never starts. However, a lambda expression attached to an event handler that returns a.Value() has not yet been evaluated. As a result, when it starts, it will receive the current value of a , which by that time is the last element.

The easiest way to make this work the way you expect, without using additional variables, etc., is likely to change to something like this:

 var buttons = n.Select(a => { Button freshButton = new Button { Content = a.Key }; freshButton.Click += (x, y) => a.Value(); return freshButton; }); foreach(var button in buttons) stackPanel.Children.Add(button); 
+1
source

To fix this, follow these steps:

 var value = a.Value(); b.Click += (x, y) => value; 
0
source

Source: https://habr.com/ru/post/1339032/


All Articles