Strange behavior of DataGrid and Message.Attach

Topics: Actions & Coroutines
May 10, 2011 at 6:35 AM
Edited May 10, 2011 at 6:38 AM

Hello,

I've noticed a strange behavior of the DataGrid with Buttons in it, to with I attach method using Message.Attach. To show you that I've created a sample project, with one ViewModel:

[Export(typeof(IShell))]
public class ShellViewModel : Screen, IShell 
{
    private ObservableCollection<ItemViewModel> _items;

    public ShellViewModel()
    {
        _items = new ObservableCollection<ItemViewModel>();

        for (int i = 0; i < 500; i++)
        {
            _items.Add(new ItemViewModel() { Number = i });
        }
    }

    public ObservableCollection<ItemViewModel> Items
    {
        get { return _items; }
        set { _items = value; }
    }
}
and a View:
<UserControl x:Class="DataGridMVVMTest1.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"  
             xmlns:g="clr-namespace:System.Windows.Data;assembly=System.Windows"
             xmlns:caliburn="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro">

	<Grid Background="White">
        <sdk:DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" >
            <sdk:DataGrid.Columns>
                <sdk:DataGridTemplateColumn>
                    <sdk:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>                            
                                <StackPanel Orientation="Horizontal">
                                    <TextBox Text="{Binding Number}" />
                                    <Button Content="Wrong result" caliburn:Message.Attach="Test" Margin="10,0,0,0" />
                                    <Button Content="Good result" caliburn:Message.Attach="Test($dataContext)" Margin="10,0,0,0" />
                            </StackPanel>                            
                        </DataTemplate>
                    </sdk:DataGridTemplateColumn.CellTemplate>
                </sdk:DataGridTemplateColumn>
            </sdk:DataGrid.Columns>
        </sdk:DataGrid>
    </Grid>
</UserControl>
There is also one additional class:
public class ItemViewModel
{
    public int Number { get; set; }

    public void Test()
    {
        //It shows wrong number
        MessageBox.Show(Number.ToString()); //This is only for test - not real MVVM approach
    }

    public void Test(ItemViewModel model)
    {
        //It shows good number
        MessageBox.Show(model.Number.ToString());
    }
}
As you can see there is a DataGrid with two buttons:
<Button Content="Wrong result" caliburn:Message.Attach="Test" Margin="10,0,0,0" />
<Button Content="Good result" caliburn:Message.Attach="Test($dataContext)" Margin="10,0,0,0" />

My problem is that when I click the 'Wrong result' button (especially somewhere in the middle of the DataGrid) the message box shows me some random number. 
When I click 'Good result' button the result is good - which is rather obvious ;)

Can someone explain me this behavior? Is it connected to the Caliburn Micro (I'm not sure of that) 
or rather virtualization of DataGrid (and others item containers in SL)?

I'm really appreciated for the answer.

You can download sample project here: http://min.us/mvfTQOq
May 11, 2011 at 12:52 PM

Yesterday I've tried to understand it, but still without success. Can anybody reproduce this behavior?

Thank you in advance!

May 12, 2011 at 10:36 AM

I was able to observe the behaviour. I'm looking into it right now. 
I agree that it could have to do with grid virtualization, but is quite strange anyway.

I'll keep you posted. 

May 12, 2011 at 11:42 AM

It definitely has to do with virtualization. I managed to reproduce it with a ListBox:

 <ListBox ItemsSource="{Binding Items}">
            <!--<ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>-->
            <ListBox.ItemTemplate>

                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBox Text="{Binding Number}" />
                        <Button Content="{Binding Number, StringFormat=w \{0:n\}}"
                                caliburn:Message.Attach="Test"
                                Margin="10,0,0,0" />
                        <Button Content="{Binding Number, StringFormat=r \{0:n\}}"
                                caliburn:Message.Attach="Test($dataContext)"
                                Margin="10,0,0,0" />

                    </StackPanel>
                </DataTemplate>

            </ListBox.ItemTemplate>

        </ListBox>

Restoring the commented part (which basically disables rows virtualization) the issue doesn't show up. 
I'm going to check if it originates from System.Interactivity doing incorrect associatiation between the event source and the trigger; I strongly hope it's not the case.

May 12, 2011 at 11:51 AM
Edited May 12, 2011 at 11:52 AM

Thank you for your response. I'm glad that you were able to reproduce it (at least I know that I'm not the only one with this problem). I can say good luck with your investigation and tell me if I can help you somehow.

Coordinator
May 12, 2011 at 1:17 PM

I'm not sure I fully understand the problem. But, if you are having an issue, you should probably change the methods names. I don't think we handle overloading in CM.

May 12, 2011 at 1:42 PM

It's not a problem with method names (in my example you can have different names, it doesn't matter). The problem is that the method without parameter (caliburn:Message.Attach="Test") is invoked on the wrong object. In other words, the binding between DataGrid item and a method is wrong.

May 12, 2011 at 1:56 PM
Edited May 12, 2011 at 1:58 PM

I've made another test:

public class ItemViewModel
{
    public int Number { get; set; }

    public void WrongMethod()
    {
        //It shows wrong number
        MessageBox.Show(Number.ToString()); //This is only for test - not real MVVM approach
    }

    public void GoodMethod(ItemViewModel model)
    {
        //It shows good number
        MessageBox.Show(model.Number.ToString());
    }
}

[Export(typeof(IShell))]
public class ShellViewModel : Screen, IShell 
{
    private ObservableCollection<ItemViewModel> _items;

    public ShellViewModel()
    {
        _items = new ObservableCollection<ItemViewModel>();

        for (int i = 0; i < 500; i++)
        {
            _items.Add(new ItemViewModel() { Number = i });
        }
    }

    public ObservableCollection<ItemViewModel> Items
    {
        get { return _items; }
        set { _items = value; }
    }
}
with this xaml:
<sdk:DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" >
    <sdk:DataGrid.Columns>
        <sdk:DataGridTemplateColumn>
            <sdk:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBox Text="{Binding Number}" />
                        <Button Content="{Binding Number, StringFormat=WrongResult \{0:n\}}" Margin="10,0,0,0" >
                            <i:Interaction.Triggers>
                                <i:EventTrigger EventName="Click">
                                    <caliburn:ActionMessage MethodName="WrongMethod">
                                    </caliburn:ActionMessage>
                                </i:EventTrigger>
                            </i:Interaction.Triggers>
                        </Button>
                        <Button Content="{Binding Number, StringFormat=GoodResult \{0:n\}}" caliburn:Message.Attach="GoodMethod($dataContext)" Margin="10,0,0,0" />
                    </StackPanel>
                </DataTemplate>
            </sdk:DataGridTemplateColumn.CellTemplate>
        </sdk:DataGridTemplateColumn>
    </sdk:DataGrid.Columns>
</sdk:DataGrid>
and it's still wrong.
Coordinator
May 12, 2011 at 2:15 PM

Actions target the Action.Target which is set by default to the VM for the View. This happens to be the DataContext as well. When it cannot find a Target with the correct method, it defaults to the DataContext. In this case, I believe it is defaulting to the first item in the list. This won't be updated, so you will get the wrong number. That's my theory at least. Try adding a target without context to the StackPanel inside your DataTemplate. It would be something like this:

<StackPanel Orientation="Horizontal" cal:Action.TargetWithoutContext="{Binding}">
That will set the Action.Target for all actions inside the stack panel to the DataContext of the template without re-setting the DataTemplate. See if that fixes your problem. 
You can read more about Action Targets here: http://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Actions&referringTitle=Documentation
May 12, 2011 at 3:05 PM

Rob, you are true: setting TargetWithoutContext does solve the issue.
This is actually due to an ActionExecutionContext being constructed around the wrong target, but it seems that ActionMessage receives it from the AssociatedObject property.
I'll make some test to confirm this.

 

May 12, 2011 at 4:29 PM

The full story:
UI virtualization seems to reuse already created list items (originally associated with VMs with lower indexes) to represent VMs with higher indexes when they get into the viewport.
Items are reused just changing the DataContext.
As a consequence an ActionMessage originally created, let's say, for the VM[1], could be lately reassociated to VM[30].
Obviously, when the action is invoked it should be the action on VM[30] while the ActionMessage actually stores a context built around VM[1].

Forcing the binding of TargetWithoutContext ensures that the ActionMessage gets notified when the datacontext of the element is changed.

May 12, 2011 at 4:44 PM
Edited May 12, 2011 at 4:46 PM

It means then that the problem was solved by you guys. Thank you Marco for full description and Rob for pointing a solution. Cheers!