2

What exactly is the difference between these two snippets:

A:

MyDataGrid.IsKeyboardFocusWithinChanged += (sender, e) => {
    if ((bool)e.NewValue == true)
    {
        Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
        {
            // do something
        }));
    }
};

B:

MyDataGrid.IsKeyboardFocusWithinChanged += (sender, e) => {
    if ((bool)e.NewValue == true)
    {
        Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() =>
        {
            // do something
        }));
    }
};

I thought the only difference between Invoke and BeginInvoke is that the first one waits for the task to be finished while the latter retuns instantly. Since the Dispatcher call is the only thing that happens in the EventHandler, I assumed that using Invoke would have the exact same effect as using BeginInvoke in this case. However I have a working example where this is clearly not the case. Take a look for yourself:

MainWindow.xaml

<Window x:Class="DataGridFocusTest.MainWindow"
        x:Name="MyWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DataGridFocusTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <TextBox DockPanel.Dock="Top"></TextBox>
        <DataGrid x:Name="MyDataGrid" DockPanel.Dock="Top" SelectionUnit="FullRow"
                  ItemsSource="{Binding SomeNumbers, ElementName=MyWindow}"
                  AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Data" Binding="{Binding Data}"
                                    IsReadOnly="True" />
                <DataGridTemplateColumn Header="CheckBox">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <CheckBox/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </DockPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace DataGridFocusTest
{
    public class MyDataClass
    {
        public string Data { get; set; }
    }

    public partial class MainWindow : Window
    {
        public IList<MyDataClass> SomeNumbers
        {
            get
            {
                Random rnd = new Random();
                List<MyDataClass> list = new List<MyDataClass>();

                for (int i = 0; i < 100; i++)
                {
                    list.Add(new MyDataClass() { Data = rnd.Next(1000).ToString() });
                }

                return list;
            }
        }

        public MainWindow()
        {
            InitializeComponent();

            MyDataGrid.IsKeyboardFocusWithinChanged += (sender, e) =>
            {
                if ((bool)e.NewValue == true)
                {
                    // whenever MyDataGrid gets KeyboardFocus set focus to
                    // the selected item
                    // this has to be delayed to prevent some weird mouse interaction
                    Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() =>
                    {
                        if (MyDataGrid.IsKeyboardFocusWithin &&
                            MyDataGrid.SelectedItem != null)
                        {
                            MyDataGrid.UpdateLayout();
                            var cellcontent = MyDataGrid.Columns[1].GetCellContent(MyDataGrid.SelectedItem);
                            var cell = cellcontent?.Parent as DataGridCell;
                            if (cell != null)
                            {
                                cell.Focus();
                            }
                        }
                    }));
                }
            };
        }
    }
}

What we have here is a DataGrid which rows include a CheckBox. It implements two specific behaviors:

  1. When the DataGrid gets keyboard focus it will focus it's selected row (which for some reason is not the default behavior).
  2. You can change the state of a CheckBox with a single click even when the DataGrid is not focused.

This works already. With the assumption at the top of the post, I thought that it would not matter whether I use Invoke or BeginInvoke. If you change the code to BeginInvoke however, for some reason the 2cnd behavior (one-click-checkbox-selection) does not work anymore.

I don't look for a solution (the solution is just using Invoke) but rather want to know why Invoke and BeginInvoke behave so differently in this case.

Tim Pohlmann
  • 4,140
  • 3
  • 32
  • 61
  • See for example: http://stackoverflow.com/questions/229554/whats-the-difference-between-invoke-and-begininvoke – Flat Eric Oct 31 '16 at 13:54
  • 1
    Invoke is synchronous. BeginInvoke is asynchronous. With Invoke, the stuff that happens after the event handler completes happens after the invoke delegate executes. With BeginInvoke, it happens *before* the invoke delegate executes. – 15ee8f99-57ff-4f92-890c-b56153 Oct 31 '16 at 13:55
  • @EdPlunkett But what is calling the EventHandler? And why does it make a difference in this case? – Tim Pohlmann Oct 31 '16 at 13:57
  • @FlatEric I know the general difference between `Invoke` and `BeginInvoke`. I was wondering about this specific case. – Tim Pohlmann Oct 31 '16 at 13:57
  • 1
    `MyDataGrid` is calling the event handler. The difference, I would *guess* (that's why this is a comment not an answer) is that IsFocusWithinChanged happens before the mouse down event. So with Invoke, the cell gets focus before the mouse event happens, so the CheckBox gets the mouse event. But BeginInvoke doesn't execute your delegate until the whole focus/mouse/etc. thing is complete. So by the time the cell has focus, the mouse event already went to somebody else and the checkbox never saw it. Easy to test, I'd imagine. Just trace in PreviewMouseDown and in your invoke delegate. – 15ee8f99-57ff-4f92-890c-b56153 Oct 31 '16 at 14:01
  • @EdPlunkett Sounds like a good guess. It surprises me that the different `EventHandlers` do wait on each other, though. Do you know whether that kind of implementation is the norm for WPF controls or if it can be considered an odditiy? – Tim Pohlmann Oct 31 '16 at 14:05
  • Why are you trying to invoke to the UI thread *when you're already in the UI thread* in the first place? Just execute the code. – Servy Oct 31 '16 at 14:11
  • @Servy Because I have to delay the task to the `Loaded` priority or it will not work as intended. – Tim Pohlmann Oct 31 '16 at 14:13
  • 2
    @TimPohlmann It's a single-threaded UI. They have to wait on each other. Moreover, if they didn't, trying to do anything event driven would be a nightmare. Imagine if a long-running MouseDown handler caused MouseUp to happen before MouseDown was finished. – 15ee8f99-57ff-4f92-890c-b56153 Oct 31 '16 at 14:20
  • @EdPlunkett Makes sense, thanks. – Tim Pohlmann Oct 31 '16 at 14:21
  • Hmm, that's backwards. Using the dispatcher loop to deal with re-entrancy bugs is an old trick. Most basic problem here is that you change the focus in a focusing event, almost never not a problem. Workaround is to let the code run after the event handling is complete. But that takes BeginInvoke, not Invoke. I suspect that you are actually taking advantage of the dispatcher queue being emptied before the invoked code can run. The DispatcherPriority looks wrong, ought to be one of the Idle values. – Hans Passant Oct 31 '16 at 14:22
  • @HansPassant "The DispatcherPriority looks wrong, ought to be one of the Idle values." Why exactly? I need to delay the task so the `DataGridCell`s are created properly. Loaded should be enough for this. But maybe I'm misunderstanding you? – Tim Pohlmann Oct 31 '16 at 14:26

0 Answers0