-1

I have a simple application that has an ObservableCollection<Item> collection bound to UniformGrid. If I use:

Items.Add(new Item 
          { 
              ID = i.ToString(), 
              Name = i.ToString(), 
              TestCommand = new RelayCommand<Item>((Item) => ChangeName(Item)) 
          });

Then the RelayCommand that is bound to the UI fires as expected but if I change the previous line to:

Application.Current.Dispatcher.BeginInvoke(
    new Action(()=>
    { 
        Items.Add(new Item 
                  {
                      ID=i.ToString(),
                      Name=i.ToString(),
                      TestCommand=new RelayCommand<Item>((Item)=>ChangeName(Item)) 
                  });
     }));

The UI does not invoke the RelayCommand. Can you explain why?

Code:

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new Data();
        }
    }

    public class Item:INotifyPropertyChanged
    {
        private string id;

        public string ID
        {
            get
            {
                return this.id;
            }

            set
            {
                this.id = value;
                RaisePropertyChanged("ID");
            }
        }        

        private string name;

        public string Name
        {
            get
            {
                return this.name;
            }

            set
            {
                this.name = value;
                this.RaisePropertyChanged("Name");
            }
        }

        public RelayCommand<Item> TestCommand { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string info)
        {
            if (PropertyChanged!=null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }        
        }
    }

    public class Data
    {
        public ObservableCollection<Item> Items { get; set; }

        public Data()
        {
            Items = new ObservableCollection<Item>();
            CreateBoxes();
        }

        public void ChangeName(Item Item)
        {
            Items.Select(x=>x.ID== Item.ID);
            Item.Name = "Changed";
        }

        public void CreateBoxes()
        {
            for (int i=0;i<4;i++)
            {
               //Application.Current.Dispatcher.BeginInvoke( new Action(()=>{ Items.Add(new Item {ID=i.ToString(),Name=i.ToString(),TestCommand=new RelayCommand<Item>((Item)=>ChangeName(Item)) });}));
                Items.Add(new Item { ID = i.ToString(), Name = i.ToString(), TestCommand = new RelayCommand<Item>((Item) => ChangeName(Item)) });
            }
        }
    }
}

Xaml code:

 <Grid>
    <ListView ItemsSource="{Binding Items}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Border BorderThickness="1" BorderBrush="LightBlue">
                    <Grid VerticalAlignment="Center" HorizontalAlignment="Center">
                        <Grid.RowDefinitions>
                            <RowDefinition/>
                            <RowDefinition/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <Label Grid.Column="0" Grid.Row="0" Content="ID:"/>
                        <Label Grid.Column="1" Grid.Row="0" Content="Name:"/>
                        <Label Grid.Column="0" Grid.Row="1" Content="{Binding ID}"/>
                        <Label Grid.Column="1" Grid.Row="1" Content="{Binding Name}"/>
                        <Button Grid.Row="2" Grid.ColumnSpan="2" Content="Test" Command="{Binding TestCommand}" CommandParameter="{Binding .}"/>
                    </Grid>
                </Border>
            </DataTemplate>
        </ListView.ItemTemplate>
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <UniformGrid Rows="2" Columns="2">
                </UniformGrid>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
    </ListView>
</Grid>
Patrick Quirk
  • 23,334
  • 2
  • 57
  • 88
A191919
  • 3,422
  • 7
  • 49
  • 93

1 Answers1

0

I am unable to reproduce the problem you describe. When I use the code you shared (adding a simple RelayCommand<T> implementation, since you omitted that), the command is invoked properly when the button is clicked, whether I call Items.Add() directly or I use BeginInvoke() to call it later.

That said, you do have a serious problem in your code: you are using the captured loop variable i for the call to BeginInvoke(), rather than a new variable local to the scope of the for loop's block. This introduces the likelihood that by the time any of the invoked delegates are executed, the loop variable will have been increased to the end of the loop value, i.e. 4.

In your example, you should change the CreateBoxes() method so that it looks more like this:

public void CreateBoxes()
{
    for (int i = 0; i < 4; i++)
    {
        string itemText = i.ToString();

        Application.Current.Dispatcher.BeginInvoke((Action)(() =>
            Items.Add(new Item
            {
                ID = itemText,
                Name = itemText,
                TestCommand = new RelayCommand<Item>(item => ChangeName(item))
            })));
    }
}

I.e. convert the ID to a string before calling BeginInvoke(). This has the effect of creating a separate variable to be captured for each iteration of the loop, ensuring that each delegate invocation gets its own private copy of the variable (and the happy side-effect of having to evaluation i.ToString() only once per loop iteration :) ).

Without a good, minimal, complete code example that actually reproduces the problem you describe (i.e. the command not being invoked by the button), I have no idea whether the above is related to that problem. Depending on what your actual code looks like, incorrect handling of captured variables could lead to problems with an invoked command, or it could be completely unrelated to this particular bug.

If the above does not help you solve your problem, please edit your question so that it includes a good code example that reliably reproduces the problem.


As an aside: in your ChangeName() method, you have a statement that appears completely superfluous: Items.Select(x=>x.ID== Item.ID);. I don't know what you meant for that to do, but all it actually does is return an IEnumerable<bool> object that, when enumerated, will return a sequence where the element is true where the ID value for the passed-in Item object is the same as that found in the respective original Items element. The code doesn't use the returned IEnumerable<bool> object for anything, never mind actually enumerates it, so the object is simply discarded and the statement has no actual effect in the program.

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136